Skip to content

Commit

Permalink
feat: add r/foo20-airdrop
Browse files Browse the repository at this point in the history
  • Loading branch information
albttx committed Jul 23, 2023
1 parent b6dc190 commit 7c384db
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 4 deletions.
88 changes: 88 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,88 @@
package airdrop

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

"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 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
}
100 changes: 100 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,100 @@
package airdrop

import (
"std"
"testing"

"gno.land/p/demo/grc/grc20"
"gno.land/p/demo/merkle"
"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)
}
}
39 changes: 39 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,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
}
65 changes: 65 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,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)
}
}
9 changes: 7 additions & 2 deletions examples/gno.land/r/demo/foo20/foo20.gno
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions examples/gno.land/r/demo/foo20/foo20_test.gno
Original file line number Diff line number Diff line change
Expand Up @@ -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) }},
Expand All @@ -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) }},
Expand Down

0 comments on commit 7c384db

Please sign in to comment.