Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: r/demo/foo20airdrop - Merkle Airdrop contract example #906

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions examples/gno.land/p/demo/airdrop/airdrop.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package airdrop

import (
"encoding/json"
"std"
)

type AirdropData interface {
Bytes() []byte
Address() std.Address
Amount() uint64
}

// Data is complient with AirdropData interface
type Data struct {
data struct {
Address std.Address `json:"address"`
Amount uint64 `json:"amount"`
}
}

func NewData(addr std.Address, amount uint64) Data {
d := Data{}
d.data.Address = addr
d.data.Amount = amount
return d
}

func (d Data) Bytes() []byte {
out, err := json.Marshal(d.data)
if err != nil {
panic(err)
}
return []byte(out)
}

func (d Data) Address() std.Address {
return d.data.Address
}

func (d Data) Amount() uint64 {
return d.data.Amount
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/p/demo/airdrop/grc20_merkle_airdrop
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package grc20_merkle_airdrop

import (
"crypto/sha256"
"encoding/hex"
"errors"

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

var (
ErrAlreadyClaimed = errors.New("already claimed")
ErrInvalidProof = errors.New("invalid merkle proof")
)

type MerkleAirdrop struct {
root string

token grc20.Token
claimed *avl.Tree
}

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

token: token,
claimed: avl.NewTree(),
}
}

func (ma *MerkleAirdrop) Root() string {
return ma.root
}

func (ma *MerkleAirdrop) Claim(data airdrop.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
}

// THIS WORKS
// foo20.Transfer(users.AddressOrName(data.Address), data.Amount)

// THIS DOES NOT WORK
// foo20.GRC20().Transfer(data.Address, data.Amount)

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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package grc20_merkle_airdrop

import (
"std"
"testing"

"gno.land/p/demo/airdrop"
"gno.land/p/demo/grc/grc20"
"gno.land/p/demo/merkle"
"gno.land/p/demo/ufmt"
"gno.land/p/demo/urequire"
)

func getLeaves(size int) []merkle.Hashable {
leaves := make([]merkle.Hashable, size)

for i := 0; i < size; i++ {
leaves[i] = airdrop.NewData(
std.DerivePkgAddr(ufmt.Sprintf("gno.land/test/%d", i)),
10000,
)
}

return leaves
}

func TestRegisterMerkle(t *testing.T) {
leaves := getLeaves(3)
tree := merkle.NewTree(leaves)
root := tree.Root()
contractAddr := std.DerivePkgAddr("gno.land/r/demo/tok20-airdrop")

token := grc20.NewBanker("TOKEN", "TOK", 6)
token.Mint(contractAddr, 50000) // Airdrop contract

tok20airdrop := NewMerkleAirdrop(root, token.Token())
_ = tok20airdrop
}

func TestClaimAirdrop(t *testing.T) {
contractAddr := std.DerivePkgAddr("gno.land/r/demo/tok20-airdrop")
std.TestSetOrigCaller(contractAddr)

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

// instantiate foo20 airdrop contract
token := grc20.NewBanker("TOKEN", "TOK", 6)
token.Mint(contractAddr, 50000) // Airdrop contract

tok20airdrop := NewMerkleAirdrop(root, token.Token())

sumClaimed := uint64(0)
for _, leaf := range leaves {
data := leaf.(airdrop.Data)

sumClaimed += data.Amount()

proofs, err := tree.Proof(leaf)
urequire.NoError(t, err)

// claim airdrop
err = tok20airdrop.Claim(data, proofs)
urequire.NoError(t, err)

balance := token.BalanceOf(data.Address())
urequire.Equal(t, balance, uint64(10000))
}

ttClaimed := tok20airdrop.TotalClaimed()
urequire.Equal(t, ttClaimed, sumClaimed)
}

func TestDoubleClaim(t *testing.T) {
leaves := getLeaves(5)

contractAddr := std.DerivePkgAddr("gno.land/r/demo/tok20-airdrop")
std.TestSetOrigCaller(contractAddr)

tree := merkle.NewTree(leaves)
token := grc20.NewBanker("TOKEN", "TOK", 6)
token.Mint(contractAddr, 50000)

tok20airdrop := NewMerkleAirdrop(tree.Root(), token.Token())

leaf := leaves[0]
proofs, err := tree.Proof(leaf)
urequire.NoError(t, err)

err = tok20airdrop.Claim(leaf.(airdrop.Data), proofs)
urequire.NoError(t, err)

err = tok20airdrop.Claim(leaf.(airdrop.Data), proofs)
urequire.Error(t, err, ErrAlreadyClaimed.Error())
}
38 changes: 38 additions & 0 deletions examples/gno.land/r/demo/foo20-airdrop/airdrop.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package foo20airdrop

import (
"gno.land/p/demo/airdrop"
"gno.land/p/demo/airdrop/grc20_merkle_airdrop"
"gno.land/p/demo/grc/grc20"
"gno.land/p/demo/merkle"
"gno.land/r/demo/foo20"
)

var (
token grc20.Token

// admin std.Address = "g1sw5xklxjjuv0yvuxy5f5s3l3mnj0nqq626a9wr" // albttx.gno
albttx marked this conversation as resolved.
Show resolved Hide resolved

foo20airdrop *grc20_merkle_airdrop.MerkleAirdrop
)

func RegisterMerkleRoot(root string) {
token = foo20.Token()
foo20airdrop = nil

if foo20airdrop != nil {
panic("foo20 airdrop merkle root is already registered")
}
foo20airdrop = grc20_merkle_airdrop.NewMerkleAirdrop(root, token)
}

func Claim(data airdrop.AirdropData, proofs []merkle.Node) {
err := foo20airdrop.Claim(data, proofs)
if err != nil {
panic(err.Error())
}
}
albttx marked this conversation as resolved.
Show resolved Hide resolved

func TotalClaimed() uint64 {
return foo20airdrop.TotalClaimed()
}
70 changes: 70 additions & 0 deletions examples/gno.land/r/demo/foo20-airdrop/airdrop_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package foo20airdrop

import (
"std"
"testing"

"gno.land/p/demo/airdrop"
"gno.land/p/demo/merkle"
"gno.land/p/demo/ufmt"
"gno.land/p/demo/urequire"
"gno.land/p/demo/users"
"gno.land/r/demo/foo20"
)

func getLeaves(size int) []merkle.Hashable {
leaves := make([]merkle.Hashable, size)

for i := 0; i < size; i++ {
leaves[i] = airdrop.NewData(
std.DerivePkgAddr(ufmt.Sprintf("gno.land/test/%d", i)),
10000,
)
}

return leaves
}

func TestRegisterMerkle(t *testing.T) {
albttx marked this conversation as resolved.
Show resolved Hide resolved
leaves := getLeaves(5)

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

RegisterMerkleRoot(root)
}

func TestClaimAirdrop(t *testing.T) {
leaves := getLeaves(5)

contractAddr := std.DerivePkgAddr("gno.land/r/demo/foo20-airdrop")
std.TestSetOrigCaller(contractAddr)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you still need that ?

p/demo/foo20 uses std.PrevRealm() now right ?


for _, leave := range leaves {
// Get 10k token
foo20.Faucet()
}

// instantiate foo20 airdrop contract
tree := merkle.NewTree(leaves)
RegisterMerkleRoot(tree.Root())

sumClaimed := uint64(0)
for _, leaf := range leaves {
data := leaf.(airdrop.Data)
user := data.Address()
sumClaimed += data.Amount()

proofs, err := tree.Proof(leaf)
urequire.NoError(t, err)

// claim airdrop
Claim(data, proofs)

addr := users.AddressOrName(data.Address())
balance := foo20.BalanceOf(addr)
urequire.Equal(t, balance, uint64(10000))
}

urequire.Equal(t, TotalClaimed(), sumClaimed)
}
1 change: 1 addition & 0 deletions examples/gno.land/r/demo/foo20-airdrop/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/r/demo/foo20-airdrop
4 changes: 4 additions & 0 deletions examples/gno.land/r/demo/foo20/foo20.gno
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ func init() {
token = banker.Token()
}

func Token() grc20.Token {
return token
}

func TotalSupply() uint64 { return token.TotalSupply() }

func BalanceOf(owner pusers.AddressOrName) uint64 {
Expand Down
Loading