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

[FEATURE] Merkle Airdrop #1

Open
wants to merge 1 commit 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
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
}
103 changes: 103 additions & 0 deletions examples/gno.land/p/demo/airdrop/airdrop_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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: "g1qhuef2450xh7g7na8s865nreu2xw8j84kgkvt5",
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, 50_000) // Airdrop contract

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

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

proof, err := tree.Proof(leaf)
if err != nil {
t.Fatalf("failed to generate proof, %v", err)
return
}

// claim airdrop
err = tok20airdrop.Claim(data, proof)
if err != nil {
t.Fatalf("error: Claim: got %s", err.Error())
}

sumClaimed += data.Amount
}

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)
}
}
89 changes: 89 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,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
}
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: "g1qhuef2450xh7g7na8s865nreu2xw8j84kgkvt5",
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)
}
}
10 changes: 8 additions & 2 deletions examples/gno.land/r/demo/foo20/foo20.gno
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,21 @@ 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)
_ = foo.GRC20()
}

// method proxies as public functions.
//

// getters.

func GRC20() grc20.IGRC20 {
return foo.GRC20()
}

func TotalSupply() uint64 {
return foo.TotalSupply()
}
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
Loading