-
Notifications
You must be signed in to change notification settings - Fork 375
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
303 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
101 changes: 101 additions & 0 deletions
101
examples/gno.land/p/demo/airdrop/merkle-airdrop_test.gno
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters