diff --git a/examples/gno.land/p/demo/airdrop/merkle-airdrop.gno b/examples/gno.land/p/demo/airdrop/merkle-airdrop.gno new file mode 100644 index 00000000000..ebfa75f2697 --- /dev/null +++ b/examples/gno.land/p/demo/airdrop/merkle-airdrop.gno @@ -0,0 +1,89 @@ +package airdrop + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "errors" + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/grc/grc20" + "gno.land/p/demo/merkle" + "gno.land/r/demo/users" +) + +var ( + ErrAlreadyClaimed = errors.New("already claimed") + ErrInvalidProof = errors.New("invalid merkle proof") +) + +type AirdropData struct { + Address std.Address + // TODO: use std.Coin + Amount uint64 + // Amount std.Coin +} + +func (data AirdropData) Bytes() []byte { + // TODO: use binary.Write + // var buf bytes.Buffer + // binary.Write(&buf, binary.BigEndian, d) + // return buf.Bytes() + // OR: use json.Marshal for frontend compatibilities + + s := fmt.Sprintf("%v", data) + return []byte(s) +} + +type MerkleAirdrop struct { + root string + + token grc20.IGRC20 + claimed *avl.Tree +} + +func NewMerkleAirdrop(merkleroot string, token grc20.IGRC20) *MerkleAirdrop { + return &MerkleAirdrop{ + root: merkleroot, + + token: token, + claimed: avl.NewTree(), + } +} + +func (ma *MerkleAirdrop) Root() string { + return ma.root +} + +func (ma *MerkleAirdrop) Claim(data AirdropData, proofs []merkle.Node) error { + shasum := sha256.Sum256(data.Bytes()) + hash := hex.EncodeToString(shasum[:]) + + if ma.claimed.Has(hash) { + return ErrAlreadyClaimed + } + + if !merkle.Verify(ma.root, data, proofs) { + return ErrInvalidProof + } + + err := ma.token.Transfer(data.Address, data.Amount) + if err != nil { + return err + } + + ma.claimed.Set(hash, data.Amount) + return nil +} + +func (ma MerkleAirdrop) TotalClaimed() uint64 { + var claimed uint64 = 0 + + ma.claimed.Iterate("", "", func(k string, v interface{}) bool { + claimed += v.(uint64) + return false + }) + + return claimed +} diff --git a/examples/gno.land/p/demo/airdrop/merkle-airdrop_test.gno b/examples/gno.land/p/demo/airdrop/merkle-airdrop_test.gno new file mode 100644 index 00000000000..277f9d52160 --- /dev/null +++ b/examples/gno.land/p/demo/airdrop/merkle-airdrop_test.gno @@ -0,0 +1,101 @@ +package airdrop + +import ( + "std" + "testing" + + "gno.land/p/demo/merkle" + "gno.land/p/demo/grc/grc20" + "gno.land/r/demo/foo20" + "gno.land/r/demo/users" +) + +var leaves []merkle.Hashable = []AirdropData{ + { + Address: "g1auhc2cymv7gn9qmls0ttdr3wqrljgz0dhq90e", + Amount: 10000, + }, + { + Address: "g1zyvskpxg5lv4qpygtuvp93zprrrjpk2exa9rfx", + Amount: 10000, + }, + { + Address: "g14szvkruznx49sxe4m9dmg3m8606sm6yp4a0wv8", + Amount: 10000, + }, +} + +func TestRegisterMerkle(t *testing.T) { + tree := merkle.NewTree(leaves) + root := tree.Root() + contractAddr := std.DerivePkgAddr("gno.land/r/demo/tok20-airdrop") + + token := grc20.NewAdminToken("TOKEN", "TOK", 6) + token.Mint(contractAddr, 50000) // Airdrop contract + + tok20airdrop := NewMerkleAirdrop(root, token.GRC20()) + +} + +func TestClaimAirdrop(t *testing.T) { + contractAddr := std.DerivePkgAddr("gno.land/r/demo/tok20-airdrop") + std.TestSetOrigCaller(contractAddr) + + // instantiate foo20 airdrop contract + tree := merkle.NewTree(leaves) + root := tree.Root() + + token := grc20.NewAdminToken("TOKEN", "TOK", 6) + token.Mint(contractAddr, 50000) // Airdrop contract + + tok20airdrop := NewMerkleAirdrop(root, token.GRC20()) + + sumClaimed := uint64(0) + for _, leaf := range leaves { + data := leaf.(AirdropData) + user := data.Address + sumClaimed += data.Amount + + proofs, err := tree.Proof(leaf) + if err != nil { + t.Fatalf("failed to generate proof, %v", err) + return + } + + // claim airdrop + tok20airdrop.Claim(data, proofs) + } + + ttClaimed := tok20airdrop.TotalClaimed() + if ttClaimed != sumClaimed { + t.Fatalf("expected: %d, got: %d", sumClaimed, ttClaimed) + } +} + +func TestDoubleClaim(t *testing.T) { + contractAddr := std.DerivePkgAddr("gno.land/r/demo/tok20-airdrop") + std.TestSetOrigCaller(contractAddr) + + tree := merkle.NewTree(leaves) + token := grc20.NewAdminToken("TOKEN", "TOK", 6) + token.Mint(contractAddr, 50000) + + tok20airdrop := NewMerkleAirdrop(tree.Root(), token.GRC20()) + + leaf := leaves[0] + proofs, err := tree.Proof(leaf) + if err != nil { + t.Fatalf("failed to generate proof, %v", err) + return + } + + err = tok20airdrop.Claim(leaf.(AirdropData), proofs) + if err != nil { + t.Fatalf("failed to claim airdrop: %v", err) + } + + err = tok20airdrop.Claim(leaf.(AirdropData), proofs) + if err != ErrAlreadyClaimed { + t.Fatalf("want: %v, got: %v", ErrAlreadyClaimed, err) + } +} diff --git a/examples/gno.land/r/demo/foo20-airdrop/airdrop.gno b/examples/gno.land/r/demo/foo20-airdrop/airdrop.gno new file mode 100644 index 00000000000..ef75e2fc403 --- /dev/null +++ b/examples/gno.land/r/demo/foo20-airdrop/airdrop.gno @@ -0,0 +1,39 @@ +package foo20airdrop + +import ( + "gno.land/p/demo/airdrop" + "gno.land/p/demo/grc/grc20" + "gno.land/p/demo/merkle" + "gno.land/r/demo/foo20" +) + +var ( + token grc20.IGRC20 = foo20.GRC20() + + // admin std.Address = "g1sw5xklxjjuv0yvuxy5f5s3l3mnj0nqq626a9wr" // albttx.gno + + foo20airdrop *airdrop.MerkleAirdrop +) + +func RegisterMerkleRoot(root string) { + if foo20airdrop != nil { + panic("foo20 airdrop merkle root is already registered") + } + foo20airdrop = airdrop.NewMerkleAirdrop(root, token) +} + +func Claim(data airdrop.AirdropData, proofs []merkle.Node) { + err := foo20airdrop.Claim(data, proofs) + if err != nil { + panic(err.Error()) + } +} + +func TotalClaimed() uint64 { + return foo20airdrop.TotalClaimed() +} + +// for tests purpose +func reset() { + foo20airdrop = nil +} diff --git a/examples/gno.land/r/demo/foo20-airdrop/airdrop_test.gno b/examples/gno.land/r/demo/foo20-airdrop/airdrop_test.gno new file mode 100644 index 00000000000..6142b619356 --- /dev/null +++ b/examples/gno.land/r/demo/foo20-airdrop/airdrop_test.gno @@ -0,0 +1,65 @@ +package foo20airdrop + +import ( + "std" + "testing" + + "gno.land/p/demo/airdrop" + "gno.land/p/demo/merkle" + "gno.land/r/demo/foo20" + "gno.land/r/demo/users" +) + +var leaves []merkle.Hashable = []airdrop.AirdropData{ + { + Address: "g1auhc2cymv7gn9qmls0ttdr3wqrljgz0dhq90e", + Amount: 1_000_000, + }, + { + Address: "g1zyvskpxg5lv4qpygtuvp93zprrrjpk2exa9rfx", + Amount: 1_000_000, + }, + { + Address: "g14szvkruznx49sxe4m9dmg3m8606sm6yp4a0wv8", + Amount: 1_000_000, + }, +} + +func TestRegisterMerkle(t *testing.T) { + tree := merkle.NewTree(leaves) + root := tree.Root() + + RegisterMerkleRoot(root) + reset() +} + +func TestClaimAirdrop(t *testing.T) { + contractAddr := std.DerivePkgAddr("gno.land/r/demo/foo20-airdrop") + std.TestSetOrigCaller(contractAddr) + + // instantiate foo20 airdrop contract + tree := merkle.NewTree(leaves) + RegisterMerkleRoot(tree.Root()) + defer reset() + + sumClaimed := uint64(0) + for _, leaf := range leaves { + data := leaf.(airdrop.AirdropData) + user := data.Address + sumClaimed += data.Amount + + proofs, err := tree.Proof(leaf) + if err != nil { + t.Fatalf("failed to generate proof, %v", err) + return + } + + // claim airdrop + Claim(leaf.(airdrop.AirdropData), proofs) + } + + ttClaimed := TotalClaimed() + if ttClaimed != sumClaimed { + t.Fatalf("expected: %d", sumClaimed) + } +} diff --git a/examples/gno.land/r/demo/foo20/foo20.gno b/examples/gno.land/r/demo/foo20/foo20.gno index 9053b1932cd..7c6b6216df7 100644 --- a/examples/gno.land/r/demo/foo20/foo20.gno +++ b/examples/gno.land/r/demo/foo20/foo20.gno @@ -16,8 +16,13 @@ var ( func init() { foo = grc20.NewAdminToken("Foo", "FOO", 4) - foo.Mint(admin, 1000000*10000) // @administrator (1M) - foo.Mint("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq", 10000*10000) // @manfred (10k) + foo.Mint(admin, 10_000_000_000) // @administrator (1M) + foo.Mint("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq", 100_000_000) // @manfred (10k) + foo.Mint(std.DerivePkgAddr("gno.land/r/demo/foo20-airdrop"), 10_000_000) +} + +func GRC20() grc20.IGRC20 { + return foo.GRC20() } // method proxies as public functions. diff --git a/examples/gno.land/r/demo/foo20/foo20_test.gno b/examples/gno.land/r/demo/foo20/foo20_test.gno index f57bff73336..6d9a3cafff6 100644 --- a/examples/gno.land/r/demo/foo20/foo20_test.gno +++ b/examples/gno.land/r/demo/foo20/foo20_test.gno @@ -21,7 +21,7 @@ func TestReadOnlyPublicMethods(t *testing.T) { // check balances #1. { tests := []test{ - {"TotalSupply", 10100000000, func() uint64 { return TotalSupply() }}, + {"TotalSupply", 10110000000, func() uint64 { return TotalSupply() }}, {"BalanceOf(admin)", 10000000000, func() uint64 { return BalanceOf(admin) }}, {"BalanceOf(manfred)", 100000000, func() uint64 { return BalanceOf(manfred) }}, {"Allowance(admin, manfred)", 0, func() uint64 { return Allowance(admin, manfred) }}, @@ -41,7 +41,7 @@ func TestReadOnlyPublicMethods(t *testing.T) { // check balances #2. { tests := []test{ - {"TotalSupply", 10110000000, func() uint64 { return TotalSupply() }}, + {"TotalSupply", 10120000000, func() uint64 { return TotalSupply() }}, {"BalanceOf(admin)", 10000000000, func() uint64 { return BalanceOf(admin) }}, {"BalanceOf(manfred)", 100000000, func() uint64 { return BalanceOf(manfred) }}, {"Allowance(admin, manfred)", 0, func() uint64 { return Allowance(admin, manfred) }},