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 17 commits into
base: master
Choose a base branch
from
41 changes: 41 additions & 0 deletions examples/gno.land/p/demo/airdrop/airdrop.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package airdrop

import (
"std"

"gno.land/p/demo/ufmt"
)

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 := ufmt.Sprintf(`{"address":"%s","amount":"%d"}`, d.data.Address, d.data.Amount)
return []byte(out)
}

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

func (d Data) Amount() uint64 {
return d.data.Amount
}
3 changes: 3 additions & 0 deletions examples/gno.land/p/demo/airdrop/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module gno.land/p/demo/airdrop

require gno.land/p/demo/ufmt v0.0.0-latest
10 changes: 10 additions & 0 deletions examples/gno.land/p/demo/airdrop/grc20_merkle_airdrop/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module gno.land/p/demo/airdrop/grc20_merkle_airdrop

require (
gno.land/p/demo/airdrop v0.0.0-latest
gno.land/p/demo/avl v0.0.0-latest
gno.land/p/demo/grc/grc20 v0.0.0-latest
gno.land/p/demo/merkle v0.0.0-latest
gno.land/p/demo/ufmt v0.0.0-latest
gno.land/p/demo/urequire v0.0.0-latest
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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.Teller
claimed *avl.Tree
}

func NewMerkleAirdrop(merkleroot string, token grc20.Teller) *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
}

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, admin := grc20.NewToken("TOKEN", "TOK", 6)
admin.Mint(contractAddr, 50000) // Airdrop contract

tok20airdrop := NewMerkleAirdrop(root, token.RealmTeller())
_ = 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, admin := grc20.NewToken("TOKEN", "TOK", 6)
admin.Mint(contractAddr, 50000) // Airdrop contract

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

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, admin := grc20.NewToken("TOKEN", "TOK", 6)
admin.Mint(contractAddr, 50000)

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

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())
}
95 changes: 95 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,95 @@
package foo20_airdrop

import (
"encoding/hex"
"std"
"strconv"

"gno.land/p/demo/airdrop"
"gno.land/p/demo/airdrop/grc20_merkle_airdrop"
"gno.land/p/demo/grc/grc20"
"gno.land/p/demo/json"
"gno.land/p/demo/merkle"
"gno.land/p/demo/ufmt"
"gno.land/r/demo/foo20"
)

var (
token grc20.Teller

foo20airdrop *grc20_merkle_airdrop.MerkleAirdrop
)

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

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())
}
}

type ClaimRequest struct {
Address string `json:"address"`
Amount string `json:"amount"`
Proof []struct {
Position float64 `json:"position"`
Data string `json:"data"`
} `json:"proof"`
}

func (req ClaimRequest) MustGetAmount() uint64 {
n, err := strconv.ParseInt(req.Amount, 10, 64)
if err != nil {
panic("failed to parse amount: " + req.Amount)
}
return uint64(n)
}

func ClaimJSON(in string) {
n, err := json.Unmarshal([]byte(in))
if err != nil {
panic(err.Error())
}

req := &ClaimRequest{
Address: n.MustKey("address").MustString(),
Amount: n.MustKey("amount").MustString(),
}

arr := n.MustKey("proof").MustArray()
proof := make([]merkle.Node, len(arr))

for i, e := range arr {
pos := e.MustKey("position").MustNumeric()
data, err := hex.DecodeString(e.MustKey("data").MustString())
if err != nil {
panic(err.Error())
}

proof[i] = merkle.NewNode([]byte(data), uint8(pos))
}

data := airdrop.NewData(std.Address(req.Address), req.MustGetAmount())

Claim(data, proof)
}

func TotalClaimed() uint64 {
return foo20airdrop.TotalClaimed()
}

func Render(path string) string {
if foo20airdrop == nil {
return "Airdrop is not registered yet"
}

return ufmt.Sprintf("total claimed: %d", foo20airdrop.TotalClaimed())
}
Loading
Loading