Skip to content

Commit

Permalink
chore: add p/airdrop
Browse files Browse the repository at this point in the history
  • Loading branch information
albttx committed Mar 21, 2023
1 parent 5448b8f commit 6abdbee
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 0 deletions.
6 changes: 6 additions & 0 deletions examples/gno.land/p/demo/airdrop/airdrop.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package airdrop

type Airdrop interface {
Claim(amount uint64, proof []string)
TotalClaimed() uint64
}
82 changes: 82 additions & 0 deletions examples/gno.land/p/demo/airdrop/merkle-airdrop.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package airdrop

import (
"crypto/sha256"
"encoding/hex"
"fmt"
"std"

"gno.land/p/demo/avl"
"gno.land/p/demo/grc/grc20"
"gno.land/p/demo/merkle"
"gno.land/r/demo/users"
)

type AirdropData struct {
Address std.Address
// TODO: use std.Coin
Amount uint64
}

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.AdminToken
owner std.Address

claimed *avl.Tree
}

func NewMerkleAirdrop(merkleroot string, token *grc20.AdminToken) *MerkleAirdrop {
return &MerkleAirdrop{
root: merkleroot,
token: token,

claimed: avl.NewTree(),
}
}

func (ma *MerkleAirdrop) Claim(data AirdropData, proofs []merkle.Node) {
callerAddr := std.GetOrigCaller()
contractAddr := std.GetOrigPkgAddr()

shasum := sha256.Sum256(data.Bytes())
hash := hex.EncodeToString(shasum[:])

if ma.claimed.Has(hash) {
panic("already claimed")
}

if !merkle.Verify(ma.root, data, proofs) {
panic("merkle proof is not valid")
}

err := ma.token.Transfer(std.GetOrigPkgAddr(), callerAddr, data.Amount)
if err != nil {
panic(err.Error())
}

ma.claimed.Set(hash, data.Amount)
}

func (ma *MerkleAirdrop) TotalClaimed() uint64 {
var claimed uint64 = 0

ma.claimed.Iterate("", "", func(n *avl.Node) bool {
claimed += n.Value().(uint64)
return false
})

return claimed
}
157 changes: 157 additions & 0 deletions examples/gno.land/p/demo/airdrop/merkle-airdrop_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package airdrop

import (
"bytes"
"encoding/json"
"fmt"
"std"
"testing"

"gno.land/p/demo/grc/grc20"
"gno.land/p/demo/merkle"
"gno.land/r/demo/users"
)

func TestRegisterMerkleRoot(t *testing.T) {
var leaves []merkle.Hashable = []AirdropData{
{
Address: "g1auhc2cymv7gn9qmls0ttdr3wqrljgz0dhq90e",
Amount: 1000000,
},
{
Address: "g1zyvskpxg5lv4qpygtuvp93zprrrjpk2exa9rfx",
Amount: 1000000,
},
{
Address: "g14szvkruznx49sxe4m9dmg3m8606sm6yp4a0wv8",
Amount: 1000000,
},
}

tree := merkle.NewTree(leaves)
root := tree.Root()

token := grc20.NewAdminToken("TOKEN", "TOK", 6)
token.Mint(std.GetOrigPkgAddr(), 50000) // Airdrop contract

ma := NewMerkleAirdrop(root, token)

expected := "3ac9a66a7610c9001151806766e928525ff44c27f37ec83a342ec2c83213fc53"
if ma.root != expected {
t.Fatalf("expected: %v; got: %s", expected, ma.root)
}
}

func TestClaim(t *testing.T) {
var leaves []merkle.Hashable = []AirdropData{
{
Address: "g1auhc2cymv7gn9qmls0ttdr3wqrljgz0dhq90e",
Amount: 10000,
},
{
Address: "g1zyvskpxg5lv4qpygtuvp93zprrrjpk2exa9rfx",
Amount: 10000,
},
{
Address: "g14szvkruznx49sxe4m9dmg3m8606sm6yp4a0wv8",
Amount: 10000,
},
{
Address: "g1auhc2cymv7gn9qmls0ttdr3wqrljgz0dhq90e",
Amount: 20000,
},
}

tree := merkle.NewTree(leaves)
root := tree.Root()

tests := []struct {
name string
data []AirdropData
expectedErr error
}{
{
name: "valid_prof_1",
data: []AirdropData{
leaves[1].(AirdropData),
},
data: []AirdropData{
leaves[1].(AirdropData),
},
},
{
name: "valid_prof_3",
data: []AirdropData{
leaves[1].(AirdropData),
leaves[2].(AirdropData),
leaves[3].(AirdropData),
},
},
{
name: "already_claimed",
data: []AirdropData{
leaves[1].(AirdropData),
leaves[1].(AirdropData),
},
expectedErr: "already claimed",
},
{
name: "invalid_proof",
data: []AirdropData{
{
Address: std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq"), // @moul
Amount: 1000000,
},
},
expectedErr: "merkle proof is not valid",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
defer func() {
r := recover()
if r != nil && r != test.expectedErr {
panic(r)
}
}()

token := grc20.NewAdminToken("TOKEN", "TOK", 6)
token.Mint(std.GetOrigPkgAddr(), 50000) // Airdrop contract

ma := NewMerkleAirdrop(root, token)

totalClaimed := uint64(0)
for _, d := range test.data {
std.TestSetOrigCaller(d.Address)

addr := users.AddressOrName(d.Address.String())

balanceContractOrig, _ := ma.token.BalanceOf(std.GetOrigPkgAddr())
balanceUserOrig, _ := ma.token.BalanceOf(d.Address)

proof, _ := tree.Proof(d)
if proof == nil {
proof = []merkle.Node{}
}
ma.Claim(d, proof)

totalClaimed += d.Amount

balanceContract, _ := ma.token.BalanceOf(std.GetOrigPkgAddr())
balanceUser, _ := ma.token.BalanceOf(d.Address)

if balanceContractOrig-d.Amount != balanceContract {
t.Fatalf("contract balance should be: %d", balanceContractOrig-d.Amount)
}

if balanceUserOrig+d.Amount != balanceUser {
t.Fatalf("user balance should be: %d", balanceUserOrig+d.Amount)
}
}
if ma.TotalClaimed() != totalClaimed {
t.Fatalf("total claimed should be: %v", totalClaimed)
}
})
}
}

0 comments on commit 6abdbee

Please sign in to comment.