From 936fd0d791adc16c3a5d419539c077935854ed2e Mon Sep 17 00:00:00 2001 From: Lee ByeongJun Date: Wed, 10 Jul 2024 16:08:14 +0900 Subject: [PATCH 01/10] snapshot --- airdrop/airdrop.gno | 108 ++++++++++++++++++++-- airdrop/airdrop_test.gno | 171 ++++++++++++++++++++++------------- airdrop/claim_check_test.gno | 2 +- 3 files changed, 209 insertions(+), 72 deletions(-) diff --git a/airdrop/airdrop.gno b/airdrop/airdrop.gno index c382697..665fd23 100644 --- a/airdrop/airdrop.gno +++ b/airdrop/airdrop.gno @@ -1,11 +1,13 @@ package airdrop import ( + "encoding/binary" "errors" "std" "time" "gno.land/p/demo/grc/grc20" + "gno.land/p/demo/merkle" "gno.land/p/demo/uint256" ) @@ -27,6 +29,39 @@ type Claim struct { Amount *uint256.Uint } +func (c Claim) Bytes() []byte { + b := make([]byte, 8+20+32) // 8 bytes for ID, 20 bytes for address, 32 bytes for amount + binary.BigEndian.PutUint64(b[:8], c.ID) + copy(b[8:28], c.Claimee[:]) + + // convert u256 to bytes + lowBits, _ := c.Amount.Uint64WithOverflow() + binary.BigEndian.PutUint64(b[28:36], lowBits) + + // shift right by 64 bits and get the next 64 bits + shiftedAmount := new(uint256.Uint).Rsh(c.Amount, 64) + midLowBits, _ := shiftedAmount.Uint64WithOverflow() + binary.BigEndian.PutUint64(b[36:44], midLowBits) + + // shift right by 128 bits and get the next 64 bits + shiftedAmount = new(uint256.Uint).Rsh(shiftedAmount, 64) + midHighBits, _ := shiftedAmount.Uint64WithOverflow() + binary.BigEndian.PutUint64(b[44:52], midHighBits) + + // shift right by 192 bits and get the next 64 bits + shiftedAmount = new(uint256.Uint).Rsh(shiftedAmount, 64) + highBits, _ := shiftedAmount.Uint64WithOverflow() + binary.BigEndian.PutUint64(b[52:60], highBits) + + return b +} + +// Snapshot represents a snapshot of the airdrop contract +type Snapshot struct { + Root string + Timestamp uint64 +} + // Config represents the airdrop configuration type Config struct { RefundableTimestamp uint64 @@ -39,16 +74,21 @@ type Airdrop struct { config Config claimedBitmap map[uint64]uint64 address std.Address + snapshots []Snapshot + claims []Claim } // NewAirdrop creates a new Airdrop instance -func NewAirdrop(token grc20.Token, config Config, addr std.Address) *Airdrop { - return &Airdrop{ +func NewAirdrop(token grc20.Token, config Config, addr std.Address, initialClaims []Claim) *Airdrop { + a := &Airdrop{ token: token, config: config, claimedBitmap: make(map[uint64]uint64), address: addr, + claims: initialClaims, } + a.CreateSnapshot() + return a } // claimIDToBitmapIndex converts a claim ID to bitmap index @@ -69,18 +109,23 @@ func (a *Airdrop) IsClaimed(claimID uint64) bool { } // Claim processes a single claim -func (a *Airdrop) Claim(claim Claim) (bool, error) { +func (a *Airdrop) Claim(claim Claim, proof []merkle.Node) bool { + // verify the claim against the latest snapshot + if !a.VerifySnapshot(claim, proof, len(a.snapshots)-1) { + panic("INVALID_PROOF") + } + word, index := a.claimIDToBitmapIndex(claim.ID) bitmap := a.claimedBitmap[word] mask := uint64(1) << index alreadyClaimed := bitmap&mask != 0 if alreadyClaimed { - return false, nil + return false } // check if the airdrop contract has enough balance to transfer - airdropBalance := a.token.BalanceOf(std.GetOrigCaller()) + airdropBalance := a.token.BalanceOf(a.address) if airdropBalance < claim.Amount.Uint64() { panic(ErrInsufficientBalance) } @@ -93,7 +138,7 @@ func (a *Airdrop) Claim(claim Claim) (bool, error) { // Update the bitmap a.claimedBitmap[word] = bitmap | mask - return true, nil + return true } // Claim64 processes up to 64 claims in a single batch @@ -177,6 +222,57 @@ func (a *Airdrop) Refund() error { return nil } +func (a *Airdrop) AddClaim(claim Claim) { + a.claims = append(a.claims, claim) + a.CreateSnapshot() +} + +func (a *Airdrop) GetLatestRoot() string { + if len(a.snapshots) == 0 { + return "" + } + return a.snapshots[len(a.snapshots)-1].Root +} + +// CreateSnapshot creates a new snapshot of the current state +func (a *Airdrop) CreateSnapshot() { + hashable := make([]merkle.Hashable, len(a.claims)) + for i, claim := range a.claims { + hashable[i] = merkle.Hashable(claim) + } + tree := merkle.NewTree(hashable) + root := tree.Root() + snapshot := Snapshot{ + Root: root, + Timestamp: uint64(timeNow().Unix()), + } + a.snapshots = append(a.snapshots, snapshot) +} + +func (a *Airdrop) GetProof(claim Claim) []merkle.Node { + hashables := make([]merkle.Hashable, len(a.claims)) + for i, c := range a.claims { + hashables[i] = c + } + + tree := merkle.NewTree(hashables) + p, err := tree.Proof(claim) + if err != nil { + panic(err) + } + + return p +} + +// Verifysnapshot verifies if a claim is valid at a specific snapshot +func (a *Airdrop) VerifySnapshot(claim Claim, proof []merkle.Node, snapshotIndex int) bool { + if snapshotIndex >= len(a.snapshots) { + return false + } + snapshot := a.snapshots[snapshotIndex] + return merkle.Verify(snapshot.Root, claim, proof) +} + // GetConfig returns the airdrop configuration func (a *Airdrop) GetConfig() Config { return a.config diff --git a/airdrop/airdrop_test.gno b/airdrop/airdrop_test.gno index fa3fad2..c0fe555 100644 --- a/airdrop/airdrop_test.gno +++ b/airdrop/airdrop_test.gno @@ -90,7 +90,7 @@ func (m *mockGRC20) TransferFrom(from, to std.Address, amount uint64) error { func TestClaimIDToBitmapIndex(t *testing.T) { address := std.Address("test") - a := NewAirdrop(nil, Config{}, address) + a := NewAirdrop(nil, Config{}, address, nil) tests := []struct { claimID uint64 @@ -114,8 +114,14 @@ func TestClaimIDToBitmapIndex(t *testing.T) { } func TestIsClaimed(t *testing.T) { + initialClaims := []Claim{ + {ID: 1, Claimee: std.Address("claimee1"), Amount: uint256.NewUint(100)}, + {ID: 2, Claimee: std.Address("claimee2"), Amount: uint256.NewUint(200)}, + {ID: 3, Claimee: std.Address("claimee3"), Amount: uint256.NewUint(300)}, + } + address := std.Address("test") - a := NewAirdrop(nil, Config{}, address) + a := NewAirdrop(nil, Config{}, address, initialClaims) // Test unclaimed if a.IsClaimed(0) { @@ -136,74 +142,74 @@ func TestIsClaimed(t *testing.T) { } } -func TestClaim(t *testing.T) { - address := std.Address("test") - mockToken := NewMockGRC20("Test Token", "TST", 18) - - airdrop := NewAirdrop(mockToken, Config{}, address) - - // Set up some initial balances - airdropAddress := std.GetOrigCaller() - claimeeAddress := std.Address("claimee") - mockToken.balances[airdropAddress] = 1000000 // Initial balance of airdrop contract - - claim := Claim{ - ID: 1, - Claimee: claimeeAddress, - Amount: uint256.NewUint(100000), - } - - // Test successful claim - claimed, err := airdrop.Claim(claim) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - if !claimed { - t.Error("Expected claim to be successful") - } - - // Check balances after claim - airdropBalance := mockToken.BalanceOf(airdropAddress) - if airdropBalance != 900000 { - t.Errorf("Expected airdrop balance to be 900000, got %d", airdropBalance) - } - claimeeBalance := mockToken.BalanceOf(claimeeAddress) - if claimeeBalance != 100000 { - t.Errorf("Expected claimee balance to be 100000, got %d", claimeeBalance) - } - - // Test claiming again (should fail) - claimed, err = airdrop.Claim(claim) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - if claimed { - t.Error("Expected claim to fail (already claimed)") - } - - // Test claim with insufficient balance - bigClaim := Claim{ - ID: 2, - Claimee: claimeeAddress, - Amount: uint256.NewUint(1000000), // More than the remaining balance - } - - defer func() { - if r := recover(); r == nil { - t.Errorf("should panic") - } else if r != ErrInsufficientBalance { - t.Errorf("Expected ErrInsufficientBalance, got %v", r) - } - }() - airdrop.Claim(bigClaim) -} +// func TestClaim(t *testing.T) { +// address := std.Address("test") +// mockToken := NewMockGRC20("Test Token", "TST", 18) + +// airdrop := NewAirdrop(mockToken, Config{}, address) + +// // Set up some initial balances +// airdropAddress := std.GetOrigCaller() +// claimeeAddress := std.Address("claimee") +// mockToken.balances[airdropAddress] = 1000000 // Initial balance of airdrop contract + +// claim := Claim{ +// ID: 1, +// Claimee: claimeeAddress, +// Amount: uint256.NewUint(100000), +// } + +// // Test successful claim +// claimed, err := airdrop.Claim(claim) +// if err != nil { +// t.Errorf("Unexpected error: %v", err) +// } +// if !claimed { +// t.Error("Expected claim to be successful") +// } + +// // Check balances after claim +// airdropBalance := mockToken.BalanceOf(airdropAddress) +// if airdropBalance != 900000 { +// t.Errorf("Expected airdrop balance to be 900000, got %d", airdropBalance) +// } +// claimeeBalance := mockToken.BalanceOf(claimeeAddress) +// if claimeeBalance != 100000 { +// t.Errorf("Expected claimee balance to be 100000, got %d", claimeeBalance) +// } + +// // Test claiming again (should fail) +// claimed, err = airdrop.Claim(claim) +// if err != nil { +// t.Errorf("Unexpected error: %v", err) +// } +// if claimed { +// t.Error("Expected claim to fail (already claimed)") +// } + +// // Test claim with insufficient balance +// bigClaim := Claim{ +// ID: 2, +// Claimee: claimeeAddress, +// Amount: uint256.NewUint(1000000), // More than the remaining balance +// } + +// defer func() { +// if r := recover(); r == nil { +// t.Errorf("should panic") +// } else if r != ErrInsufficientBalance { +// t.Errorf("Expected ErrInsufficientBalance, got %v", r) +// } +// }() +// airdrop.Claim(bigClaim) +// } func TestClaim64(t *testing.T) { address := std.Address("test") mockToken := NewMockGRC20("Test Token", "TST", 18) // Create a new Airdrop instance - airdrop := NewAirdrop(mockToken, Config{}, address) + airdrop := NewAirdrop(mockToken, Config{}, address, nil) // Set up some initial balances airdropAddress := std.GetOrigCaller() @@ -298,7 +304,7 @@ func TestRefund(t *testing.T) { RefundableTimestamp: uint64(time.Now().Add(time.Hour).Unix()), RefundTo: refundTo, } - airdrop := NewAirdrop(token, config, testutils.TestAddress("airdrop")) + airdrop := NewAirdrop(token, config, testutils.TestAddress("airdrop"), nil) // Set some balance for the airdrop contract token.balances[airdrop.address] = 1000 @@ -348,3 +354,38 @@ func TestRefund(t *testing.T) { // airdrop.Refund() // }) } + +func TestAirdropWithMerkle(t *testing.T) { + token := NewMockGRC20("Test Token", "TST", 18) + config := Config{ + RefundableTimestamp: 0, + RefundTo: std.Address("refund_to"), + } + + initialClaims := []Claim{ + {ID: 1, Claimee: std.Address("claimee1"), Amount: uint256.NewUint(100)}, + {ID: 2, Claimee: std.Address("claimee2"), Amount: uint256.NewUint(200)}, + {ID: 3, Claimee: std.Address("claimee3"), Amount: uint256.NewUint(300)}, + } + + airdrop := NewAirdrop(token, config, std.Address("airdrop"), initialClaims) + + // Test initial snapshot + if root := airdrop.GetLatestRoot(); root == "" { + t.Errorf("Expected non-empty root, got empty string") + } + + // Test adding a new claim + newClaim := Claim{ID: 4, Claimee: std.Address("claimee4"), Amount: uint256.NewUint(400)} + airdrop.AddClaim(newClaim) + + if len(airdrop.snapshots) != 2 { + t.Errorf("Expected 2 snapshots, got %d", len(airdrop.snapshots)) + } + + // Test claim verification + proof := airdrop.GetProof(newClaim) + if !airdrop.VerifySnapshot(newClaim, proof, 1) { + t.Errorf("Failed to verify valid claim") + } +} diff --git a/airdrop/claim_check_test.gno b/airdrop/claim_check_test.gno index 96f0db8..626a42a 100644 --- a/airdrop/claim_check_test.gno +++ b/airdrop/claim_check_test.gno @@ -14,7 +14,7 @@ func TestAirdropClaimCheck(t *testing.T) { // Create an airdrop instance airdropAddress := std.Address("airdrop_address") - airdrop := NewAirdrop(mockToken, Config{}, airdropAddress) + airdrop := NewAirdrop(mockToken, Config{}, airdropAddress, nil) // Set up some initial balances mockToken.balances[airdropAddress] = 1000000 // Initial balance of airdrop contract From e953e0b72d659d7da52502c10d34e2de4c3b8bf2 Mon Sep 17 00:00:00 2001 From: Lee ByeongJun Date: Wed, 10 Jul 2024 16:24:37 +0900 Subject: [PATCH 02/10] add more tests --- airdrop/airdrop_test.gno | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/airdrop/airdrop_test.gno b/airdrop/airdrop_test.gno index c0fe555..2dbe104 100644 --- a/airdrop/airdrop_test.gno +++ b/airdrop/airdrop_test.gno @@ -355,20 +355,38 @@ func TestRefund(t *testing.T) { // }) } -func TestAirdropWithMerkle(t *testing.T) { - token := NewMockGRC20("Test Token", "TST", 18) - config := Config{ - RefundableTimestamp: 0, - RefundTo: std.Address("refund_to"), - } +func setupAirdrop() (*Airdrop, *mockGRC20) { + token := NewMockGRC20("Test Token", "TST", 18) + config := Config{ + RefundableTimestamp: 0, + RefundTo: std.Address("refund_to"), + } + + initialClaims := []Claim{ + {ID: 1, Claimee: std.Address("claimee1"), Amount: uint256.NewUint(100)}, + {ID: 2, Claimee: std.Address("claimee2"), Amount: uint256.NewUint(200)}, + {ID: 3, Claimee: std.Address("claimee3"), Amount: uint256.NewUint(300)}, + } + + airdrop := NewAirdrop(token, config, std.Address("airdrop"), initialClaims) + return airdrop, token +} - initialClaims := []Claim{ - {ID: 1, Claimee: std.Address("claimee1"), Amount: uint256.NewUint(100)}, - {ID: 2, Claimee: std.Address("claimee2"), Amount: uint256.NewUint(200)}, - {ID: 3, Claimee: std.Address("claimee3"), Amount: uint256.NewUint(300)}, +func TestRootConsistency(t *testing.T) { + airdrop, _ := setupAirdrop() + initialRoot := airdrop.snapshots[0].Root + + newClaim := Claim{ID: 4, Claimee: std.Address("claimee4"), Amount: uint256.NewUint(400)} + airdrop.AddClaim(newClaim) + + newRoot := airdrop.snapshots[1].Root + if initialRoot == newRoot { + t.Errorf("Expected new root to be different from initial root") } +} - airdrop := NewAirdrop(token, config, std.Address("airdrop"), initialClaims) +func TestAirdropWithMerkle(t *testing.T) { + airdrop, token := setupAirdrop() // Test initial snapshot if root := airdrop.GetLatestRoot(); root == "" { From ad1c832fdb2ad7985adc6fca557147874e41a888 Mon Sep 17 00:00:00 2001 From: Lee ByeongJun Date: Wed, 10 Jul 2024 18:24:56 +0900 Subject: [PATCH 03/10] save --- airdrop/airdrop.gno | 6 +- airdrop/airdrop_test.gno | 119 +++++++++++++++++++-------------------- 2 files changed, 59 insertions(+), 66 deletions(-) diff --git a/airdrop/airdrop.gno b/airdrop/airdrop.gno index 665fd23..100d0b8 100644 --- a/airdrop/airdrop.gno +++ b/airdrop/airdrop.gno @@ -256,11 +256,7 @@ func (a *Airdrop) GetProof(claim Claim) []merkle.Node { } tree := merkle.NewTree(hashables) - p, err := tree.Proof(claim) - if err != nil { - panic(err) - } - + p, _ := tree.Proof(claim) return p } diff --git a/airdrop/airdrop_test.gno b/airdrop/airdrop_test.gno index 2dbe104..4f4f8be 100644 --- a/airdrop/airdrop_test.gno +++ b/airdrop/airdrop_test.gno @@ -142,67 +142,64 @@ func TestIsClaimed(t *testing.T) { } } -// func TestClaim(t *testing.T) { -// address := std.Address("test") -// mockToken := NewMockGRC20("Test Token", "TST", 18) - -// airdrop := NewAirdrop(mockToken, Config{}, address) - -// // Set up some initial balances -// airdropAddress := std.GetOrigCaller() -// claimeeAddress := std.Address("claimee") -// mockToken.balances[airdropAddress] = 1000000 // Initial balance of airdrop contract - -// claim := Claim{ -// ID: 1, -// Claimee: claimeeAddress, -// Amount: uint256.NewUint(100000), -// } - -// // Test successful claim -// claimed, err := airdrop.Claim(claim) -// if err != nil { -// t.Errorf("Unexpected error: %v", err) -// } -// if !claimed { -// t.Error("Expected claim to be successful") -// } - -// // Check balances after claim -// airdropBalance := mockToken.BalanceOf(airdropAddress) -// if airdropBalance != 900000 { -// t.Errorf("Expected airdrop balance to be 900000, got %d", airdropBalance) -// } -// claimeeBalance := mockToken.BalanceOf(claimeeAddress) -// if claimeeBalance != 100000 { -// t.Errorf("Expected claimee balance to be 100000, got %d", claimeeBalance) -// } - -// // Test claiming again (should fail) -// claimed, err = airdrop.Claim(claim) -// if err != nil { -// t.Errorf("Unexpected error: %v", err) -// } -// if claimed { -// t.Error("Expected claim to fail (already claimed)") -// } - -// // Test claim with insufficient balance -// bigClaim := Claim{ -// ID: 2, -// Claimee: claimeeAddress, -// Amount: uint256.NewUint(1000000), // More than the remaining balance -// } - -// defer func() { -// if r := recover(); r == nil { -// t.Errorf("should panic") -// } else if r != ErrInsufficientBalance { -// t.Errorf("Expected ErrInsufficientBalance, got %v", r) -// } -// }() -// airdrop.Claim(bigClaim) -// } +func TestClaim(t *testing.T) { + address := std.Address("test") + mockToken := NewMockGRC20("Test Token", "TST", 18) + + initialClaims := []Claim{ + {ID: 1, Claimee: std.Address("claimee"), Amount: uint256.NewUint(100000)}, + {ID: 2, Claimee: std.Address("claimee2"), Amount: uint256.NewUint(200000)}, + {ID: 3, Claimee: std.Address("claimee3"), Amount: uint256.NewUint(300000)}, + } + + airdrop := NewAirdrop(mockToken, Config{}, address, nil) + + // Set up some initial balances + airdropAddress := address + claimeeAddress := std.Address("claimee") + mockToken.balances[airdropAddress] = 1000000 // Initial balance of airdrop contract + + claim := initialClaims[0] + proof := airdrop.GetProof(claim) + + // Test successful claim + // claimed := airdrop.Claim(claim, proof) + // if !claimed { + // t.Error("Expected claim to be successful") + // } + + // Check balances after claim + // airdropBalance := mockToken.BalanceOf(airdropAddress) + // if airdropBalance != 900000 { + // t.Errorf("Expected airdrop balance to be 900000, got %d", airdropBalance) + // } + // claimeeBalance := mockToken.BalanceOf(claimeeAddress) + // if claimeeBalance != 100000 { + // t.Errorf("Expected claimee balance to be 100000, got %d", claimeeBalance) + // } + + // // Test claiming again (should fail) + // claimed = airdrop.Claim(claim, proof) + // if claimed { + // t.Error("Expected claim to fail (already claimed)") + // } + + // // Test claim with insufficient balance + // bigClaim := Claim{ + // ID: 2, + // Claimee: claimeeAddress, + // Amount: uint256.NewUint(1000000), // More than the remaining balance + // } + + // defer func() { + // if r := recover(); r == nil { + // t.Errorf("should panic") + // } else if r != ErrInsufficientBalance { + // t.Errorf("Expected ErrInsufficientBalance, got %v", r) + // } + // }() + // airdrop.Claim(bigClaim, proof) +} func TestClaim64(t *testing.T) { address := std.Address("test") From cbca21f3621a63aaf4c2a96b05c2f78ed1002158 Mon Sep 17 00:00:00 2001 From: Lee ByeongJun Date: Wed, 10 Jul 2024 18:52:08 +0900 Subject: [PATCH 04/10] find problem. need to fix later --- airdrop/airdrop.gno | 6 +++ airdrop/airdrop_test.gno | 112 ++++++++++++++++++++++----------------- 2 files changed, 69 insertions(+), 49 deletions(-) diff --git a/airdrop/airdrop.gno b/airdrop/airdrop.gno index 100d0b8..1de0007 100644 --- a/airdrop/airdrop.gno +++ b/airdrop/airdrop.gno @@ -138,6 +138,12 @@ func (a *Airdrop) Claim(claim Claim, proof []merkle.Node) bool { // Update the bitmap a.claimedBitmap[word] = bitmap | mask + std.Emit("Claimed", + "id", string(claim.ID), + "claimee", claim.Claimee.String(), + "amount", claim.Amount.ToString(), + ) + return true } diff --git a/airdrop/airdrop_test.gno b/airdrop/airdrop_test.gno index 4f4f8be..e59cc8d 100644 --- a/airdrop/airdrop_test.gno +++ b/airdrop/airdrop_test.gno @@ -56,9 +56,11 @@ func (m *mockGRC20) BalanceOf(account std.Address) uint64 { func (m *mockGRC20) Transfer(to std.Address, amount uint64) error { from := std.GetOrigCaller() - if m.balances[from] < amount { - return errors.New("insufficient balance") - } + // xxx + // panic(amount) + // if m.balances[from] < amount { + // return errors.New("insufficient balance") + // } m.balances[from] -= amount m.balances[to] += amount return nil @@ -143,62 +145,74 @@ func TestIsClaimed(t *testing.T) { } func TestClaim(t *testing.T) { - address := std.Address("test") - mockToken := NewMockGRC20("Test Token", "TST", 18) + // Setup + token := NewMockGRC20("Test Token", "TST", 18) + airdropAddress := testutils.TestAddress("airdrop") + refundTo := testutils.TestAddress("refund_to") + config := Config{ + RefundableTimestamp: uint64(time.Now().Add(24 * time.Hour).Unix()), + RefundTo: refundTo, + } - initialClaims := []Claim{ - {ID: 1, Claimee: std.Address("claimee"), Amount: uint256.NewUint(100000)}, - {ID: 2, Claimee: std.Address("claimee2"), Amount: uint256.NewUint(200000)}, - {ID: 3, Claimee: std.Address("claimee3"), Amount: uint256.NewUint(300000)}, + // Create initial claims + claims := []Claim{ + {ID: 0, Claimee: testutils.TestAddress("user1"), Amount: uint256.NewUint(100)}, + {ID: 1, Claimee: testutils.TestAddress("user2"), Amount: uint256.NewUint(200)}, + {ID: 2, Claimee: testutils.TestAddress("user3"), Amount: uint256.NewUint(300)}, } - airdrop := NewAirdrop(mockToken, Config{}, address, nil) + airdrop := NewAirdrop(token, config, airdropAddress, claims) - // Set up some initial balances - airdropAddress := address - claimeeAddress := std.Address("claimee") - mockToken.balances[airdropAddress] = 1000000 // Initial balance of airdrop contract + // Set initial balance for airdrop contract + token.balances[airdropAddress] = 1000 - claim := initialClaims[0] - proof := airdrop.GetProof(claim) + t.Run("Successful claim", func(t *testing.T) { + claim := claims[0] + proof := airdrop.GetProof(claim) - // Test successful claim - // claimed := airdrop.Claim(claim, proof) - // if !claimed { - // t.Error("Expected claim to be successful") - // } + result := airdrop.Claim(claim, proof) - // Check balances after claim - // airdropBalance := mockToken.BalanceOf(airdropAddress) - // if airdropBalance != 900000 { - // t.Errorf("Expected airdrop balance to be 900000, got %d", airdropBalance) - // } - // claimeeBalance := mockToken.BalanceOf(claimeeAddress) - // if claimeeBalance != 100000 { - // t.Errorf("Expected claimee balance to be 100000, got %d", claimeeBalance) - // } + if !result { + t.Errorf("Expected successful claim, but got false") + } - // // Test claiming again (should fail) - // claimed = airdrop.Claim(claim, proof) - // if claimed { - // t.Error("Expected claim to fail (already claimed)") - // } + if !airdrop.IsClaimed(claim.ID) { + t.Errorf("Expected claim to be marked as claimed") + } - // // Test claim with insufficient balance - // bigClaim := Claim{ - // ID: 2, - // Claimee: claimeeAddress, - // Amount: uint256.NewUint(1000000), // More than the remaining balance - // } + if token.balances[claim.Claimee] != claim.Amount.Uint64() { + t.Errorf("Expected claimee balance to be %d, but got %d", claim.Amount.Uint64(), token.balances[claim.Claimee]) + } + }) + + t.Run("Claim already processed", func(t *testing.T) { + claim := claims[0] + proof := airdrop.GetProof(claim) + + result := airdrop.Claim(claim, proof) + + if result { + t.Errorf("Expected claim to fail as it was already processed, but got true") + } + }) + + t.Run("Insufficient balance", func(t *testing.T) { + // Set airdrop balance to 0 + token.balances[airdropAddress] = 0 + + claim := claims[2] + proof := airdrop.GetProof(claim) - // defer func() { - // if r := recover(); r == nil { - // t.Errorf("should panic") - // } else if r != ErrInsufficientBalance { - // t.Errorf("Expected ErrInsufficientBalance, got %v", r) - // } - // }() - // airdrop.Claim(bigClaim, proof) + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected function to panic with ErrInsufficientBalance, but it didn't") + } else if r != ErrInsufficientBalance { + t.Errorf("Expected ErrInsufficientBalance panic, got %v", r) + } + }() + + airdrop.Claim(claim, proof) + }) } func TestClaim64(t *testing.T) { From 78e9b1fb80200f19e8d11cabaf2edcea9d1b36d2 Mon Sep 17 00:00:00 2001 From: Lee ByeongJun Date: Thu, 11 Jul 2024 16:23:50 +0900 Subject: [PATCH 05/10] save --- airdrop/airdrop.gno | 60 +++++++++-------- airdrop/airdrop_test.gno | 135 ++++++++++++++++++++++++++++++++++----- 2 files changed, 150 insertions(+), 45 deletions(-) diff --git a/airdrop/airdrop.gno b/airdrop/airdrop.gno index 1de0007..4a5e83e 100644 --- a/airdrop/airdrop.gno +++ b/airdrop/airdrop.gno @@ -125,11 +125,12 @@ func (a *Airdrop) Claim(claim Claim, proof []merkle.Node) bool { } // check if the airdrop contract has enough balance to transfer - airdropBalance := a.token.BalanceOf(a.address) - if airdropBalance < claim.Amount.Uint64() { + airdropBalance := uint256.NewUint(a.token.BalanceOf(a.address)) + if claim.Amount.Gt(airdropBalance) { panic(ErrInsufficientBalance) } + // FIXME. Uint64 method may be problematic err := a.token.Transfer(claim.Claimee, claim.Amount.Uint64()) if err != nil { panic(err) @@ -147,8 +148,7 @@ func (a *Airdrop) Claim(claim Claim, proof []merkle.Node) bool { return true } -// Claim64 processes up to 64 claims in a single batch -func (a *Airdrop) Claim64(claims []Claim) (uint8, error) { +func (a *Airdrop) Claim64(claims []Claim, proofs [][]merkle.Node) uint8 { if len(claims) == 0 || len(claims) > 64 { panic(ErrInvalidClaimsCount) } @@ -157,28 +157,32 @@ func (a *Airdrop) Claim64(claims []Claim) (uint8, error) { panic(ErrFirstClaimIsNot64) } + if len(claims) != len(proofs) { + panic("INVALID_PROOFS_COUNT") + } + word := claims[0].ID / 64 bitmap := a.claimedBitmap[word] - var ( - claimedCount uint8 - totalAmount uint64 - ) + var claimedCount uint8 + var totalAmount uint256.Uint - // calculate total amount and check if any claim is already processed + // verify all claims and calculate total amount for i, claim := range claims { + if !a.VerifySnapshot(claim, proofs[i], len(a.snapshots)-1) { + panic("INVALID_PROOF") + } + index := uint64(i) mask := uint64(1) << index - alreadyClaimed := bitmap&mask != 0 - - if !alreadyClaimed { - totalAmount += claim.Amount.Uint64() + if bitmap&mask == 0 { + totalAmount.Add(&totalAmount, claim.Amount) } } - // check has enough balance to transfer - airdropBalance := a.token.BalanceOf(std.GetOrigCaller()) - if airdropBalance < totalAmount { + // check if the contract has enough balance to transfer + airdropBalance := a.token.BalanceOf(a.address) + if uint256.NewUint(airdropBalance).Cmp(&totalAmount) < 0 { panic(ErrInsufficientBalance) } @@ -187,22 +191,22 @@ func (a *Airdrop) Claim64(claims []Claim) (uint8, error) { index := uint64(i) mask := uint64(1) << index - alreadyClaimed := bitmap&mask != 0 - if alreadyClaimed { - continue + if bitmap&mask == 0 { + a.token.Transfer(claim.Claimee, claim.Amount.Uint64()) + bitmap |= mask + claimedCount++ + + std.Emit( + "Claimed", + "id", string(claim.ID), + "claimee", claim.Claimee.String(), + "amount", claim.Amount.ToString(), + ) } - - err := a.token.Transfer(claim.Claimee, claim.Amount.Uint64()) - if err != nil { - return claimedCount, err - } - - bitmap |= mask - claimedCount++ } a.claimedBitmap[word] = bitmap - return claimedCount, nil + return claimedCount } // Refund refunds the remaining tokens to the specified address after the refundable timestamp diff --git a/airdrop/airdrop_test.gno b/airdrop/airdrop_test.gno index e59cc8d..62da7cd 100644 --- a/airdrop/airdrop_test.gno +++ b/airdrop/airdrop_test.gno @@ -56,11 +56,9 @@ func (m *mockGRC20) BalanceOf(account std.Address) uint64 { func (m *mockGRC20) Transfer(to std.Address, amount uint64) error { from := std.GetOrigCaller() - // xxx - // panic(amount) - // if m.balances[from] < amount { - // return errors.New("insufficient balance") - // } + if m.balances[from] < amount { + panic(ErrInsufficientBalance) + } m.balances[from] -= amount m.balances[to] += amount return nil @@ -219,13 +217,6 @@ func TestClaim64(t *testing.T) { address := std.Address("test") mockToken := NewMockGRC20("Test Token", "TST", 18) - // Create a new Airdrop instance - airdrop := NewAirdrop(mockToken, Config{}, address, nil) - - // Set up some initial balances - airdropAddress := std.GetOrigCaller() - mockToken.balances[airdropAddress] = 1000000 // Initial balance of airdrop contract - // Create 64 claims claims := make([]Claim, 64) for i := 0; i < 64; i++ { @@ -236,8 +227,21 @@ func TestClaim64(t *testing.T) { } } + // Create a new Airdrop instance with initial claims + airdrop := NewAirdrop(mockToken, Config{}, address, claims) + + // Set up some initial balances + airdropAddress := airdrop.GetAddress() + mockToken.balances[airdropAddress] = 1000000 // Initial balance of airdrop contract + + // Generate proofs for all claims + proofs := make([][]merkle.Node, 64) + for i, claim := range claims { + proofs[i] = airdrop.GetProof(claim) + } + // Test successful batch claim - claimed, _ := airdrop.Claim64(claims) + claimed := airdrop.Claim64(claims, proofs) if claimed != 64 { t.Errorf("Expected 64 claims, got %d", claimed) } @@ -255,19 +259,21 @@ func TestClaim64(t *testing.T) { } // Test claiming again (should claim 0) - claimed, _ = airdrop.Claim64(claims) + claimed = airdrop.Claim64(claims, proofs) if claimed != 0 { t.Errorf("Expected 0 claims, got %d", claimed) } // Test with insufficient balance bigClaims := make([]Claim, 64) + bigProofs := make([][]merkle.Node, 64) for i := 0; i < 64; i++ { bigClaims[i] = Claim{ ID: uint64((i + 1) * 64), Claimee: std.Address(ufmt.Sprintf("claimee%d", i)), Amount: uint256.NewUint(20000), // Each claim is for 20000 tokens, which is more than the remaining balance } + bigProofs[i] = airdrop.GetProof(bigClaims[i]) } defer func() { @@ -278,10 +284,11 @@ func TestClaim64(t *testing.T) { } }() - airdrop.Claim64(bigClaims) + airdrop.Claim64(bigClaims, bigProofs) // Test with invalid claim count invalidClaims := make([]Claim, 65) // 65 claims, which is more than allowed + invalidProofs := make([][]merkle.Node, 65) defer func() { if r := recover(); r == nil { @@ -291,11 +298,12 @@ func TestClaim64(t *testing.T) { } }() - airdrop.Claim64(invalidClaims) + airdrop.Claim64(invalidClaims, invalidProofs) // Test with invalid first claim ID invalidFirstClaim := make([]Claim, 64) invalidFirstClaim[0] = Claim{ID: 1} // First claim ID is not a multiple of 64 + invalidFirstProofs := make([][]merkle.Node, 64) defer func() { if r := recover(); r == nil { @@ -305,9 +313,102 @@ func TestClaim64(t *testing.T) { } }() - airdrop.Claim64(invalidFirstClaim) + airdrop.Claim64(invalidFirstClaim, invalidFirstProofs) } +// func TestClaim64(t *testing.T) { +// address := std.Address("test") +// mockToken := NewMockGRC20("Test Token", "TST", 18) + +// // Create a new Airdrop instance +// airdrop := NewAirdrop(mockToken, Config{}, address, nil) + +// // Set up some initial balances +// airdropAddress := std.GetOrigCaller() +// mockToken.balances[airdropAddress] = 1000000 // Initial balance of airdrop contract + +// // Create 64 claims +// claims := make([]Claim, 64) +// for i := 0; i < 64; i++ { +// claims[i] = Claim{ +// ID: uint64(i * 64), // Ensure first claim ID is multiple of 64 +// Claimee: std.Address(ufmt.Sprintf("claimee%d", i)), +// Amount: uint256.NewUint(1000), // Each claim is for 1000 tokens +// } +// } + +// // Test successful batch claim +// claimed, _ := airdrop.Claim64(claims) +// if claimed != 64 { +// t.Errorf("Expected 64 claims, got %d", claimed) +// } + +// // Check balances after claims +// airdropBalance := mockToken.BalanceOf(airdropAddress) +// if airdropBalance != 936000 { // 1000000 - (64 * 1000) +// t.Errorf("Expected airdrop balance to be 936000, got %d", airdropBalance) +// } +// for i := 0; i < 64; i++ { +// claimeeBalance := mockToken.BalanceOf(std.Address(ufmt.Sprintf("claimee%d", i))) +// if claimeeBalance != 1000 { +// t.Errorf("Expected claimee%d balance to be 1000, got %d", i, claimeeBalance) +// } +// } + +// // Test claiming again (should claim 0) +// claimed, _ = airdrop.Claim64(claims) +// if claimed != 0 { +// t.Errorf("Expected 0 claims, got %d", claimed) +// } + +// // Test with insufficient balance +// bigClaims := make([]Claim, 64) +// for i := 0; i < 64; i++ { +// bigClaims[i] = Claim{ +// ID: uint64((i + 1) * 64), +// Claimee: std.Address(ufmt.Sprintf("claimee%d", i)), +// Amount: uint256.NewUint(20000), // Each claim is for 20000 tokens, which is more than the remaining balance +// } +// } + +// defer func() { +// if r := recover(); r == nil { +// t.Errorf("The code did not panic") +// } else if r != ErrInsufficientBalance { +// t.Errorf("Expected ErrInsufficientBalance panic, got %v", r) +// } +// }() + +// airdrop.Claim64(bigClaims) + +// // Test with invalid claim count +// invalidClaims := make([]Claim, 65) // 65 claims, which is more than allowed + +// defer func() { +// if r := recover(); r == nil { +// t.Errorf("The code did not panic") +// } else if r != ErrInvalidClaimsCount { +// t.Errorf("Expected ErrInvalidClaimsCount panic, got %v", r) +// } +// }() + +// airdrop.Claim64(invalidClaims) + +// // Test with invalid first claim ID +// invalidFirstClaim := make([]Claim, 64) +// invalidFirstClaim[0] = Claim{ID: 1} // First claim ID is not a multiple of 64 + +// defer func() { +// if r := recover(); r == nil { +// t.Errorf("The code did not panic") +// } else if r != ErrFirstClaimIsNot64 { +// t.Errorf("Expected ErrFirstClaimIsNot64 panic, got %v", r) +// } +// }() + +// airdrop.Claim64(invalidFirstClaim) +// } + func TestRefund(t *testing.T) { token := NewMockGRC20("Test Token", "TST", 18) refundTo := testutils.TestAddress("refund_to") From a5582a8cd8ca7f052dfaef40a32e07cc30b86c9c Mon Sep 17 00:00:00 2001 From: Lee ByeongJun Date: Mon, 5 Aug 2024 14:56:02 +0900 Subject: [PATCH 06/10] finishing --- airdrop/airdrop.gno | 199 +++++----------- airdrop/airdrop_test.gno | 435 +++++++++++++++-------------------- airdrop/claim_check_test.gno | 2 +- airdrop/gno.mod | 9 + gno.mod | 1 - snapshot/gno.mod | 1 + snapshot/snapshot.gno | 84 +++++++ snapshot/snapshot_test.gno | 136 +++++++++++ staker/gno.mod | 8 + 9 files changed, 477 insertions(+), 398 deletions(-) create mode 100644 airdrop/gno.mod delete mode 100644 gno.mod create mode 100644 snapshot/gno.mod create mode 100644 snapshot/snapshot.gno create mode 100644 snapshot/snapshot_test.gno create mode 100644 staker/gno.mod diff --git a/airdrop/airdrop.gno b/airdrop/airdrop.gno index 4a5e83e..14f8167 100644 --- a/airdrop/airdrop.gno +++ b/airdrop/airdrop.gno @@ -1,14 +1,14 @@ package airdrop import ( - "encoding/binary" "errors" "std" "time" "gno.land/p/demo/grc/grc20" - "gno.land/p/demo/merkle" "gno.land/p/demo/uint256" + + "gno.land/r/governance/snapshot" ) var ( @@ -29,39 +29,6 @@ type Claim struct { Amount *uint256.Uint } -func (c Claim) Bytes() []byte { - b := make([]byte, 8+20+32) // 8 bytes for ID, 20 bytes for address, 32 bytes for amount - binary.BigEndian.PutUint64(b[:8], c.ID) - copy(b[8:28], c.Claimee[:]) - - // convert u256 to bytes - lowBits, _ := c.Amount.Uint64WithOverflow() - binary.BigEndian.PutUint64(b[28:36], lowBits) - - // shift right by 64 bits and get the next 64 bits - shiftedAmount := new(uint256.Uint).Rsh(c.Amount, 64) - midLowBits, _ := shiftedAmount.Uint64WithOverflow() - binary.BigEndian.PutUint64(b[36:44], midLowBits) - - // shift right by 128 bits and get the next 64 bits - shiftedAmount = new(uint256.Uint).Rsh(shiftedAmount, 64) - midHighBits, _ := shiftedAmount.Uint64WithOverflow() - binary.BigEndian.PutUint64(b[44:52], midHighBits) - - // shift right by 192 bits and get the next 64 bits - shiftedAmount = new(uint256.Uint).Rsh(shiftedAmount, 64) - highBits, _ := shiftedAmount.Uint64WithOverflow() - binary.BigEndian.PutUint64(b[52:60], highBits) - - return b -} - -// Snapshot represents a snapshot of the airdrop contract -type Snapshot struct { - Root string - Timestamp uint64 -} - // Config represents the airdrop configuration type Config struct { RefundableTimestamp uint64 @@ -70,25 +37,22 @@ type Config struct { // Airdrop represents the airdrop contract type Airdrop struct { - token grc20.Token - config Config - claimedBitmap map[uint64]uint64 - address std.Address - snapshots []Snapshot - claims []Claim + token grc20.Token + config Config + claimedBitmap map[uint64]uint64 + address std.Address + snapshotManager *snapshot.Manager } // NewAirdrop creates a new Airdrop instance -func NewAirdrop(token grc20.Token, config Config, addr std.Address, initialClaims []Claim) *Airdrop { - a := &Airdrop{ - token: token, - config: config, - claimedBitmap: make(map[uint64]uint64), - address: addr, - claims: initialClaims, +func NewAirdrop(token grc20.Token, config Config, addr std.Address) *Airdrop { + return &Airdrop{ + token: token, + config: config, + claimedBitmap: make(map[uint64]uint64), + address: addr, + snapshotManager: snapshot.NewManager(), } - a.CreateSnapshot() - return a } // claimIDToBitmapIndex converts a claim ID to bitmap index @@ -109,28 +73,22 @@ func (a *Airdrop) IsClaimed(claimID uint64) bool { } // Claim processes a single claim -func (a *Airdrop) Claim(claim Claim, proof []merkle.Node) bool { - // verify the claim against the latest snapshot - if !a.VerifySnapshot(claim, proof, len(a.snapshots)-1) { - panic("INVALID_PROOF") - } - +func (a *Airdrop) Claim(claim Claim) (bool, error) { word, index := a.claimIDToBitmapIndex(claim.ID) bitmap := a.claimedBitmap[word] mask := uint64(1) << index alreadyClaimed := bitmap&mask != 0 if alreadyClaimed { - return false + return false, nil } // check if the airdrop contract has enough balance to transfer - airdropBalance := uint256.NewUint(a.token.BalanceOf(a.address)) - if claim.Amount.Gt(airdropBalance) { + airdropBalance := a.token.BalanceOf(std.GetOrigCaller()) + if airdropBalance < claim.Amount.Uint64() { panic(ErrInsufficientBalance) } - // FIXME. Uint64 method may be problematic err := a.token.Transfer(claim.Claimee, claim.Amount.Uint64()) if err != nil { panic(err) @@ -139,16 +97,11 @@ func (a *Airdrop) Claim(claim Claim, proof []merkle.Node) bool { // Update the bitmap a.claimedBitmap[word] = bitmap | mask - std.Emit("Claimed", - "id", string(claim.ID), - "claimee", claim.Claimee.String(), - "amount", claim.Amount.ToString(), - ) - - return true + return true, nil } -func (a *Airdrop) Claim64(claims []Claim, proofs [][]merkle.Node) uint8 { +// Claim64 processes up to 64 claims in a single batch +func (a *Airdrop) Claim64(claims []Claim) (uint8, error) { if len(claims) == 0 || len(claims) > 64 { panic(ErrInvalidClaimsCount) } @@ -157,32 +110,28 @@ func (a *Airdrop) Claim64(claims []Claim, proofs [][]merkle.Node) uint8 { panic(ErrFirstClaimIsNot64) } - if len(claims) != len(proofs) { - panic("INVALID_PROOFS_COUNT") - } - word := claims[0].ID / 64 bitmap := a.claimedBitmap[word] - var claimedCount uint8 - var totalAmount uint256.Uint + var ( + claimedCount uint8 + totalAmount uint64 + ) - // verify all claims and calculate total amount + // calculate total amount and check if any claim is already processed for i, claim := range claims { - if !a.VerifySnapshot(claim, proofs[i], len(a.snapshots)-1) { - panic("INVALID_PROOF") - } - index := uint64(i) mask := uint64(1) << index - if bitmap&mask == 0 { - totalAmount.Add(&totalAmount, claim.Amount) + alreadyClaimed := bitmap&mask != 0 + + if !alreadyClaimed { + totalAmount += claim.Amount.Uint64() } } - // check if the contract has enough balance to transfer - airdropBalance := a.token.BalanceOf(a.address) - if uint256.NewUint(airdropBalance).Cmp(&totalAmount) < 0 { + // check has enough balance to transfer + airdropBalance := a.token.BalanceOf(std.GetOrigCaller()) + if airdropBalance < totalAmount { panic(ErrInsufficientBalance) } @@ -191,22 +140,22 @@ func (a *Airdrop) Claim64(claims []Claim, proofs [][]merkle.Node) uint8 { index := uint64(i) mask := uint64(1) << index - if bitmap&mask == 0 { - a.token.Transfer(claim.Claimee, claim.Amount.Uint64()) - bitmap |= mask - claimedCount++ - - std.Emit( - "Claimed", - "id", string(claim.ID), - "claimee", claim.Claimee.String(), - "amount", claim.Amount.ToString(), - ) + alreadyClaimed := bitmap&mask != 0 + if alreadyClaimed { + continue + } + + err := a.token.Transfer(claim.Claimee, claim.Amount.Uint64()) + if err != nil { + return claimedCount, err } + + bitmap |= mask + claimedCount++ } a.claimedBitmap[word] = bitmap - return claimedCount + return claimedCount, nil } // Refund refunds the remaining tokens to the specified address after the refundable timestamp @@ -232,58 +181,26 @@ func (a *Airdrop) Refund() error { return nil } -func (a *Airdrop) AddClaim(claim Claim) { - a.claims = append(a.claims, claim) - a.CreateSnapshot() +// GetConfig returns the airdrop configuration +func (a *Airdrop) GetConfig() Config { + return a.config } -func (a *Airdrop) GetLatestRoot() string { - if len(a.snapshots) == 0 { - return "" - } - return a.snapshots[len(a.snapshots)-1].Root +func (a *Airdrop) GetAddress() std.Address { + return a.address } // CreateSnapshot creates a new snapshot of the current state -func (a *Airdrop) CreateSnapshot() { - hashable := make([]merkle.Hashable, len(a.claims)) - for i, claim := range a.claims { - hashable[i] = merkle.Hashable(claim) - } - tree := merkle.NewTree(hashable) - root := tree.Root() - snapshot := Snapshot{ - Root: root, - Timestamp: uint64(timeNow().Unix()), - } - a.snapshots = append(a.snapshots, snapshot) -} - -func (a *Airdrop) GetProof(claim Claim) []merkle.Node { - hashables := make([]merkle.Hashable, len(a.claims)) - for i, c := range a.claims { - hashables[i] = c - } - - tree := merkle.NewTree(hashables) - p, _ := tree.Proof(claim) - return p -} - -// Verifysnapshot verifies if a claim is valid at a specific snapshot -func (a *Airdrop) VerifySnapshot(claim Claim, proof []merkle.Node, snapshotIndex int) bool { - if snapshotIndex >= len(a.snapshots) { - return false - } - snapshot := a.snapshots[snapshotIndex] - return merkle.Verify(snapshot.Root, claim, proof) +func (a *Airdrop) CreateSnapshot() uint64 { + return a.snapshotManager.CreateSnapshot(a.claimedBitmap, a.token.BalanceOf(std.GetOrigCaller())) } -// GetConfig returns the airdrop configuration -func (a *Airdrop) GetConfig() Config { - return a.config +// IsClaimedAtSnapshot checks if a claim was made at a specific snapshot +func (a *Airdrop) IsClaimedAtSnapshot(snapshotID uint64, claimID uint64) (bool, error) { + return a.snapshotManager.IsClaimedAtSnapshot(snapshotID, claimID) } -func (a *Airdrop) GetAddress() std.Address { - return a.address +// GetSnapshot retrieves a specific snapshot +func (a *Airdrop) GetSnapshot(id uint64) (snapshot.Snapshot, error) { + return a.snapshotManager.GetSnapshot(id) } diff --git a/airdrop/airdrop_test.gno b/airdrop/airdrop_test.gno index 62da7cd..96765ae 100644 --- a/airdrop/airdrop_test.gno +++ b/airdrop/airdrop_test.gno @@ -57,7 +57,7 @@ func (m *mockGRC20) BalanceOf(account std.Address) uint64 { func (m *mockGRC20) Transfer(to std.Address, amount uint64) error { from := std.GetOrigCaller() if m.balances[from] < amount { - panic(ErrInsufficientBalance) + return errors.New("insufficient balance") } m.balances[from] -= amount m.balances[to] += amount @@ -90,7 +90,7 @@ func (m *mockGRC20) TransferFrom(from, to std.Address, amount uint64) error { func TestClaimIDToBitmapIndex(t *testing.T) { address := std.Address("test") - a := NewAirdrop(nil, Config{}, address, nil) + a := NewAirdrop(nil, Config{}, address) tests := []struct { claimID uint64 @@ -114,14 +114,8 @@ func TestClaimIDToBitmapIndex(t *testing.T) { } func TestIsClaimed(t *testing.T) { - initialClaims := []Claim{ - {ID: 1, Claimee: std.Address("claimee1"), Amount: uint256.NewUint(100)}, - {ID: 2, Claimee: std.Address("claimee2"), Amount: uint256.NewUint(200)}, - {ID: 3, Claimee: std.Address("claimee3"), Amount: uint256.NewUint(300)}, - } - address := std.Address("test") - a := NewAirdrop(nil, Config{}, address, initialClaims) + a := NewAirdrop(nil, Config{}, address) // Test unclaimed if a.IsClaimed(0) { @@ -143,80 +137,78 @@ func TestIsClaimed(t *testing.T) { } func TestClaim(t *testing.T) { - // Setup - token := NewMockGRC20("Test Token", "TST", 18) - airdropAddress := testutils.TestAddress("airdrop") - refundTo := testutils.TestAddress("refund_to") - config := Config{ - RefundableTimestamp: uint64(time.Now().Add(24 * time.Hour).Unix()), - RefundTo: refundTo, - } - - // Create initial claims - claims := []Claim{ - {ID: 0, Claimee: testutils.TestAddress("user1"), Amount: uint256.NewUint(100)}, - {ID: 1, Claimee: testutils.TestAddress("user2"), Amount: uint256.NewUint(200)}, - {ID: 2, Claimee: testutils.TestAddress("user3"), Amount: uint256.NewUint(300)}, - } - - airdrop := NewAirdrop(token, config, airdropAddress, claims) - - // Set initial balance for airdrop contract - token.balances[airdropAddress] = 1000 + address := std.Address("test") + mockToken := NewMockGRC20("Test Token", "TST", 18) - t.Run("Successful claim", func(t *testing.T) { - claim := claims[0] - proof := airdrop.GetProof(claim) + airdrop := NewAirdrop(mockToken, Config{}, address) - result := airdrop.Claim(claim, proof) + // Set up some initial balances + airdropAddress := std.GetOrigCaller() + claimeeAddress := std.Address("claimee") + mockToken.balances[airdropAddress] = 1000000 // Initial balance of airdrop contract - if !result { - t.Errorf("Expected successful claim, but got false") - } + claim := Claim{ + ID: 1, + Claimee: claimeeAddress, + Amount: uint256.NewUint(100000), + } - if !airdrop.IsClaimed(claim.ID) { - t.Errorf("Expected claim to be marked as claimed") - } + // Test successful claim + claimed, err := airdrop.Claim(claim) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if !claimed { + t.Error("Expected claim to be successful") + } - if token.balances[claim.Claimee] != claim.Amount.Uint64() { - t.Errorf("Expected claimee balance to be %d, but got %d", claim.Amount.Uint64(), token.balances[claim.Claimee]) - } - }) + // Check balances after claim + airdropBalance := mockToken.BalanceOf(airdropAddress) + if airdropBalance != 900000 { + t.Errorf("Expected airdrop balance to be 900000, got %d", airdropBalance) + } + claimeeBalance := mockToken.BalanceOf(claimeeAddress) + if claimeeBalance != 100000 { + t.Errorf("Expected claimee balance to be 100000, got %d", claimeeBalance) + } - t.Run("Claim already processed", func(t *testing.T) { - claim := claims[0] - proof := airdrop.GetProof(claim) + // Test claiming again (should fail) + claimed, err = airdrop.Claim(claim) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if claimed { + t.Error("Expected claim to fail (already claimed)") + } - result := airdrop.Claim(claim, proof) + // Test claim with insufficient balance + bigClaim := Claim{ + ID: 2, + Claimee: claimeeAddress, + Amount: uint256.NewUint(1000000), // More than the remaining balance + } - if result { - t.Errorf("Expected claim to fail as it was already processed, but got true") + defer func() { + if r := recover(); r == nil { + t.Errorf("should panic") + } else if r != ErrInsufficientBalance { + t.Errorf("Expected ErrInsufficientBalance, got %v", r) } - }) - - t.Run("Insufficient balance", func(t *testing.T) { - // Set airdrop balance to 0 - token.balances[airdropAddress] = 0 - - claim := claims[2] - proof := airdrop.GetProof(claim) - - defer func() { - if r := recover(); r == nil { - t.Errorf("Expected function to panic with ErrInsufficientBalance, but it didn't") - } else if r != ErrInsufficientBalance { - t.Errorf("Expected ErrInsufficientBalance panic, got %v", r) - } - }() - - airdrop.Claim(claim, proof) - }) + }() + airdrop.Claim(bigClaim) } func TestClaim64(t *testing.T) { address := std.Address("test") mockToken := NewMockGRC20("Test Token", "TST", 18) + // Create a new Airdrop instance + airdrop := NewAirdrop(mockToken, Config{}, address) + + // Set up some initial balances + airdropAddress := std.GetOrigCaller() + mockToken.balances[airdropAddress] = 1000000 // Initial balance of airdrop contract + // Create 64 claims claims := make([]Claim, 64) for i := 0; i < 64; i++ { @@ -227,21 +219,8 @@ func TestClaim64(t *testing.T) { } } - // Create a new Airdrop instance with initial claims - airdrop := NewAirdrop(mockToken, Config{}, address, claims) - - // Set up some initial balances - airdropAddress := airdrop.GetAddress() - mockToken.balances[airdropAddress] = 1000000 // Initial balance of airdrop contract - - // Generate proofs for all claims - proofs := make([][]merkle.Node, 64) - for i, claim := range claims { - proofs[i] = airdrop.GetProof(claim) - } - // Test successful batch claim - claimed := airdrop.Claim64(claims, proofs) + claimed, _ := airdrop.Claim64(claims) if claimed != 64 { t.Errorf("Expected 64 claims, got %d", claimed) } @@ -259,21 +238,19 @@ func TestClaim64(t *testing.T) { } // Test claiming again (should claim 0) - claimed = airdrop.Claim64(claims, proofs) + claimed, _ = airdrop.Claim64(claims) if claimed != 0 { t.Errorf("Expected 0 claims, got %d", claimed) } // Test with insufficient balance bigClaims := make([]Claim, 64) - bigProofs := make([][]merkle.Node, 64) for i := 0; i < 64; i++ { bigClaims[i] = Claim{ ID: uint64((i + 1) * 64), Claimee: std.Address(ufmt.Sprintf("claimee%d", i)), Amount: uint256.NewUint(20000), // Each claim is for 20000 tokens, which is more than the remaining balance } - bigProofs[i] = airdrop.GetProof(bigClaims[i]) } defer func() { @@ -284,11 +261,10 @@ func TestClaim64(t *testing.T) { } }() - airdrop.Claim64(bigClaims, bigProofs) + airdrop.Claim64(bigClaims) // Test with invalid claim count invalidClaims := make([]Claim, 65) // 65 claims, which is more than allowed - invalidProofs := make([][]merkle.Node, 65) defer func() { if r := recover(); r == nil { @@ -298,12 +274,11 @@ func TestClaim64(t *testing.T) { } }() - airdrop.Claim64(invalidClaims, invalidProofs) + airdrop.Claim64(invalidClaims) // Test with invalid first claim ID invalidFirstClaim := make([]Claim, 64) invalidFirstClaim[0] = Claim{ID: 1} // First claim ID is not a multiple of 64 - invalidFirstProofs := make([][]merkle.Node, 64) defer func() { if r := recover(); r == nil { @@ -313,209 +288,159 @@ func TestClaim64(t *testing.T) { } }() - airdrop.Claim64(invalidFirstClaim, invalidFirstProofs) + airdrop.Claim64(invalidFirstClaim) } -// func TestClaim64(t *testing.T) { -// address := std.Address("test") -// mockToken := NewMockGRC20("Test Token", "TST", 18) - -// // Create a new Airdrop instance -// airdrop := NewAirdrop(mockToken, Config{}, address, nil) - -// // Set up some initial balances -// airdropAddress := std.GetOrigCaller() -// mockToken.balances[airdropAddress] = 1000000 // Initial balance of airdrop contract - -// // Create 64 claims -// claims := make([]Claim, 64) -// for i := 0; i < 64; i++ { -// claims[i] = Claim{ -// ID: uint64(i * 64), // Ensure first claim ID is multiple of 64 -// Claimee: std.Address(ufmt.Sprintf("claimee%d", i)), -// Amount: uint256.NewUint(1000), // Each claim is for 1000 tokens -// } -// } - -// // Test successful batch claim -// claimed, _ := airdrop.Claim64(claims) -// if claimed != 64 { -// t.Errorf("Expected 64 claims, got %d", claimed) -// } - -// // Check balances after claims -// airdropBalance := mockToken.BalanceOf(airdropAddress) -// if airdropBalance != 936000 { // 1000000 - (64 * 1000) -// t.Errorf("Expected airdrop balance to be 936000, got %d", airdropBalance) -// } -// for i := 0; i < 64; i++ { -// claimeeBalance := mockToken.BalanceOf(std.Address(ufmt.Sprintf("claimee%d", i))) -// if claimeeBalance != 1000 { -// t.Errorf("Expected claimee%d balance to be 1000, got %d", i, claimeeBalance) -// } -// } - -// // Test claiming again (should claim 0) -// claimed, _ = airdrop.Claim64(claims) -// if claimed != 0 { -// t.Errorf("Expected 0 claims, got %d", claimed) -// } - -// // Test with insufficient balance -// bigClaims := make([]Claim, 64) -// for i := 0; i < 64; i++ { -// bigClaims[i] = Claim{ -// ID: uint64((i + 1) * 64), -// Claimee: std.Address(ufmt.Sprintf("claimee%d", i)), -// Amount: uint256.NewUint(20000), // Each claim is for 20000 tokens, which is more than the remaining balance -// } -// } - -// defer func() { -// if r := recover(); r == nil { -// t.Errorf("The code did not panic") -// } else if r != ErrInsufficientBalance { -// t.Errorf("Expected ErrInsufficientBalance panic, got %v", r) -// } -// }() - -// airdrop.Claim64(bigClaims) - -// // Test with invalid claim count -// invalidClaims := make([]Claim, 65) // 65 claims, which is more than allowed - -// defer func() { -// if r := recover(); r == nil { -// t.Errorf("The code did not panic") -// } else if r != ErrInvalidClaimsCount { -// t.Errorf("Expected ErrInvalidClaimsCount panic, got %v", r) -// } -// }() - -// airdrop.Claim64(invalidClaims) - -// // Test with invalid first claim ID -// invalidFirstClaim := make([]Claim, 64) -// invalidFirstClaim[0] = Claim{ID: 1} // First claim ID is not a multiple of 64 - -// defer func() { -// if r := recover(); r == nil { -// t.Errorf("The code did not panic") -// } else if r != ErrFirstClaimIsNot64 { -// t.Errorf("Expected ErrFirstClaimIsNot64 panic, got %v", r) -// } -// }() - -// airdrop.Claim64(invalidFirstClaim) -// } - func TestRefund(t *testing.T) { - token := NewMockGRC20("Test Token", "TST", 18) - refundTo := testutils.TestAddress("refund_to") - config := Config{ - RefundableTimestamp: uint64(time.Now().Add(time.Hour).Unix()), - RefundTo: refundTo, - } - airdrop := NewAirdrop(token, config, testutils.TestAddress("airdrop"), nil) + mockToken := NewMockGRC20("Test Token", "TST", 18) + refundTo := std.Address("refund_to") + airdropAddr := std.Address("airdrop_address") + + // Set up initial balances + mockToken.balances[airdropAddr] = 1000000 + + t.Run("Refund not allowed", func(t *testing.T) { + config := Config{ + RefundableTimestamp: 0, + RefundTo: refundTo, + } + airdrop := NewAirdrop(mockToken, config, airdropAddr) + + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } else if r != ErrRefundNotAllowed { + t.Errorf("Expected ErrRefundNotAllowed, got %v", r) + } + }() + airdrop.Refund() + }) - // Set some balance for the airdrop contract - token.balances[airdrop.address] = 1000 + t.Run("Refund too early", func(t *testing.T) { + config := Config{ + RefundableTimestamp: uint64(time.Now().Add(time.Hour).Unix()), + RefundTo: refundTo, + } + airdrop := NewAirdrop(mockToken, config, airdropAddr) - t.Run("Refund before refundable timestamp", func(t *testing.T) { defer func() { if r := recover(); r == nil { - t.Errorf("Expected panic but got none") + t.Errorf("The code did not panic") } else if r != ErrTooEarly { - t.Errorf("Expected ErrTooEarly panic but got: %v", r) + t.Errorf("Expected ErrTooEarly, got %v", r) + } + }() + airdrop.Refund() + }) + + t.Run("No balance to refund", func(t *testing.T) { + config := Config{ + RefundableTimestamp: uint64(time.Now().Add(-time.Hour).Unix()), + RefundTo: refundTo, + } + airdrop := NewAirdrop(mockToken, config, airdropAddr) + mockToken.balances[airdropAddr] = 0 + + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } else if r != ErrNoBalance { + t.Errorf("Expected ErrNoBalance, got %v", r) } }() airdrop.Refund() }) t.Run("Successful refund", func(t *testing.T) { - // Set current time to after refundable timestamp - oldTimeNow := timeNow - timeNow = func() time.Time { - return time.Unix(int64(config.RefundableTimestamp+1), 0) + config := Config{ + RefundableTimestamp: uint64(time.Now().Add(-time.Hour).Unix()), + RefundTo: refundTo, } - defer func() { timeNow = oldTimeNow }() + airdrop := NewAirdrop(mockToken, config, airdropAddr) + mockToken.balances[airdropAddr] = 1000000 err := airdrop.Refund() if err != nil { t.Errorf("Unexpected error: %v", err) } - if balance := token.balances[refundTo]; balance != 1000 { - t.Errorf("Expected refund balance to be 1000, got %d", balance) + if mockToken.balances[refundTo] != 1000000 { + t.Errorf("Expected refund balance to be 1000000, got %d", mockToken.balances[refundTo]) } - if balance := token.balances[airdrop.address]; balance != 0 { - t.Errorf("Expected airdrop balance to be 0, got %d", balance) + if mockToken.balances[airdropAddr] != 0 { + t.Errorf("Expected airdrop balance to be 0, got %d", mockToken.balances[airdropAddr]) } }) - - // t.Run("Refund with no balance", func(t *testing.T) { - // token.balances[airdrop.address] = 0 - // defer func() { - // if r := recover(); r == nil { - // t.Errorf("Expected panic but got none") - // } else if r != ErrNoBalance { - // t.Errorf("Expected ErrNoBalance panic but got: %v", r) - // } - // }() - // airdrop.Refund() - // }) } -func setupAirdrop() (*Airdrop, *mockGRC20) { - token := NewMockGRC20("Test Token", "TST", 18) - config := Config{ - RefundableTimestamp: 0, - RefundTo: std.Address("refund_to"), - } - - initialClaims := []Claim{ - {ID: 1, Claimee: std.Address("claimee1"), Amount: uint256.NewUint(100)}, - {ID: 2, Claimee: std.Address("claimee2"), Amount: uint256.NewUint(200)}, - {ID: 3, Claimee: std.Address("claimee3"), Amount: uint256.NewUint(300)}, - } - - airdrop := NewAirdrop(token, config, std.Address("airdrop"), initialClaims) - return airdrop, token -} +func TestAirdropWithSnapshot(t *testing.T) { + address := std.Address("test_airdrop") + mockToken := NewMockGRC20("Test Token", "TST", 18) -func TestRootConsistency(t *testing.T) { - airdrop, _ := setupAirdrop() - initialRoot := airdrop.snapshots[0].Root + airdrop := NewAirdrop(mockToken, Config{}, address) - newClaim := Claim{ID: 4, Claimee: std.Address("claimee4"), Amount: uint256.NewUint(400)} - airdrop.AddClaim(newClaim) + // Set up some initial balances + airdropAddress := std.GetOrigCaller() + mockToken.balances[airdropAddress] = 1000000 // Initial balance of airdrop contract - newRoot := airdrop.snapshots[1].Root - if initialRoot == newRoot { - t.Errorf("Expected new root to be different from initial root") + // Create 128 claims (2 batches of 64) + claims := make([]Claim, 128) + for i := 0; i < 128; i++ { + claims[i] = Claim{ + ID: uint64(i * 64), // Ensure first claim ID is multiple of 64 + Claimee: std.Address(ufmt.Sprintf("claimee%d", i)), + Amount: uint256.NewUint(1000), // Each claim is for 1000 tokens + } } -} -func TestAirdropWithMerkle(t *testing.T) { - airdrop, token := setupAirdrop() + // Process first batch of claims + claimed, _ := airdrop.Claim64(claims[:64]) + if claimed != 64 { + t.Errorf("Expected 64 claims, got %d", claimed) + } + + // Create a snapshot after first batch + snapshotID := airdrop.CreateSnapshot() + + // Process second batch of claims + claimed, _ = airdrop.Claim64(claims[64:]) + if claimed != 64 { + t.Errorf("Expected 64 claims, got %d", claimed) + } - // Test initial snapshot - if root := airdrop.GetLatestRoot(); root == "" { - t.Errorf("Expected non-empty root, got empty string") + // Check balances after all claims + airdropBalance := mockToken.BalanceOf(airdropAddress) + if airdropBalance != 872000 { // 1000000 - (128 * 1000) + t.Errorf("Expected airdrop balance to be 872000, got %d", airdropBalance) } - // Test adding a new claim - newClaim := Claim{ID: 4, Claimee: std.Address("claimee4"), Amount: uint256.NewUint(400)} - airdrop.AddClaim(newClaim) + // Test snapshot state (simplified) + sampleClaimIDs := []uint64{0, 63, 64, 127} + for _, claimID := range sampleClaimIDs { + expectedClaimed := claimID < 64 + isClaimed, err := airdrop.IsClaimedAtSnapshot(snapshotID, claimID) + if err != nil { + t.Errorf("Unexpected error checking claim status: %v", err) + } + if isClaimed != expectedClaimed { + t.Errorf("For claim %d, expected claimed status %v, got %v", claimID, expectedClaimed, isClaimed) + } + } - if len(airdrop.snapshots) != 2 { - t.Errorf("Expected 2 snapshots, got %d", len(airdrop.snapshots)) + // Test invalid snapshot ID + _, err := airdrop.IsClaimedAtSnapshot(snapshotID+1, 0) + if err == nil { + t.Error("Expected error for invalid snapshot ID, got nil") } - // Test claim verification - proof := airdrop.GetProof(newClaim) - if !airdrop.VerifySnapshot(newClaim, proof, 1) { - t.Errorf("Failed to verify valid claim") + // Get snapshot and check remaining tokens + snapshot, err := airdrop.GetSnapshot(snapshotID) + if err != nil { + t.Errorf("Unexpected error getting snapshot: %v", err) + } + expectedRemainingTokens := uint64(936000) // 1000000 - (64 * 1000) + if snapshot.RemainingTokens != expectedRemainingTokens { + t.Errorf("Expected remaining tokens %d, got %d", expectedRemainingTokens, snapshot.RemainingTokens) } } diff --git a/airdrop/claim_check_test.gno b/airdrop/claim_check_test.gno index 626a42a..96f0db8 100644 --- a/airdrop/claim_check_test.gno +++ b/airdrop/claim_check_test.gno @@ -14,7 +14,7 @@ func TestAirdropClaimCheck(t *testing.T) { // Create an airdrop instance airdropAddress := std.Address("airdrop_address") - airdrop := NewAirdrop(mockToken, Config{}, airdropAddress, nil) + airdrop := NewAirdrop(mockToken, Config{}, airdropAddress) // Set up some initial balances mockToken.balances[airdropAddress] = 1000000 // Initial balance of airdrop contract diff --git a/airdrop/gno.mod b/airdrop/gno.mod new file mode 100644 index 0000000..d09f26b --- /dev/null +++ b/airdrop/gno.mod @@ -0,0 +1,9 @@ +module gno.land/r/governance/airdrop + +require ( + gno.land/p/demo/grc/grc20 v0.0.0-latest + gno.land/p/demo/merkle v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/p/demo/uint256 v0.0.0-latest +) diff --git a/gno.mod b/gno.mod deleted file mode 100644 index 6540134..0000000 --- a/gno.mod +++ /dev/null @@ -1 +0,0 @@ -module gno.land/r/demo/governance diff --git a/snapshot/gno.mod b/snapshot/gno.mod new file mode 100644 index 0000000..606b22e --- /dev/null +++ b/snapshot/gno.mod @@ -0,0 +1 @@ +module gno.land/r/governance/snapshot diff --git a/snapshot/snapshot.gno b/snapshot/snapshot.gno new file mode 100644 index 0000000..a2b21ce --- /dev/null +++ b/snapshot/snapshot.gno @@ -0,0 +1,84 @@ +package snapshot + +import ( + "errors" + "time" +) + +// Snapshot represents a point-in-time capture of the state of a system. +type Snapshot struct { + ID uint64 + Timestamp int64 + ClaimedBitmap map[uint64]uint64 + RemainingTokens uint64 +} + +// Manager manages the creation and retrieval of snapshots. +type Manager struct { + snapshots []Snapshot + lastSnapshotID uint64 +} + +func NewManager() *Manager { + return &Manager{ + snapshots: make([]Snapshot, 0), + lastSnapshotID: 0, + } +} + +// CreateSnapshot generates a new snapshot with the given claimed bitmap and remaining tokens. +// +// It returns the ID of the newly created snapshot. +func (m *Manager) CreateSnapshot(claimedBitmap map[uint64]uint64, remainingTokens uint64) uint64 { + m.lastSnapshotID++ + snapshot := Snapshot{ + ID: m.lastSnapshotID, + Timestamp: time.Now().Unix(), + ClaimedBitmap: make(map[uint64]uint64), + RemainingTokens: remainingTokens, + } + + // deep copy for current claimedBitmap + for k, v := range claimedBitmap { + snapshot.ClaimedBitmap[k] = v + } + + m.snapshots = append(m.snapshots, snapshot) + return m.lastSnapshotID +} + +// GetSnapshot retrieves a snapshot by its ID. +// +// It returns the snapshot if found, otherwise an error. +func (m *Manager) GetSnapshot(id uint64) (Snapshot, error) { + for _, snapshot := range m.snapshots { + if snapshot.ID == id { + return snapshot, nil + } + } + return Snapshot{}, errors.New("snapshot not found") +} + +// IsClaimedAtSnapshot checks if a specifix claim was made at the time of a given snapshot. +// +// It returns true if the claim was made, false if not. +func (m *Manager) IsClaimedAtSnapshot(snapshotID, claimID uint64) (bool, error) { + snapshot, err := m.GetSnapshot(snapshotID) + if err != nil { + return false, err + } + + word, index := claimIDToBitmapIndex(claimID) + bitmap, exists := snapshot.ClaimedBitmap[word] + if !exists { + return false, nil + } + + return bitmap&(1< after { + t.Errorf("Expected timestamp between %d and %d, got %d", before, after, snapshot.Timestamp) + } +} diff --git a/staker/gno.mod b/staker/gno.mod new file mode 100644 index 0000000..ee56d33 --- /dev/null +++ b/staker/gno.mod @@ -0,0 +1,8 @@ +module gno.land/r/governance/staker + +require ( + gno.land/p/demo/grc/grc20 v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/p/demo/uint256 v0.0.0-latest +) From 9e0be0ee7f6379fece18ab91e864b0ce27fdd60d Mon Sep 17 00:00:00 2001 From: Lee ByeongJun Date: Tue, 6 Aug 2024 14:45:20 +0900 Subject: [PATCH 07/10] update README --- snapshot/README.md | 65 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 snapshot/README.md diff --git a/snapshot/README.md b/snapshot/README.md new file mode 100644 index 0000000..bc3e983 --- /dev/null +++ b/snapshot/README.md @@ -0,0 +1,65 @@ +# Snapshot + +## Overview + +The snapshot package provides functionality to capture and mange the state of a system at specific points in time. This package allows for creating, storing, and retrieving snapshots that include claimed bitmaps and the number of remaining tokens. + +## Features + +- Create and store snapshots +- Retrieve snapshots by ID +- Check claim status at a specific snapshot point + +## How it Works + +1. The `Manager` type manages a list of snapshots and the last snapshot ID. +2. Each new snapshot is assigned a unique ID and timestamped with the current time. +3. Each snapshot stores a claimed bitmap[^1] and the number of remaining tokens. + +## Example + +```go +package main + +import ( + "gno.land/p/demo/ufmt" + "gno.land/r/governance/snapshot" +) + +func main() { + // Create a new Manager + manager := snapshot.NewManager() + + // Create a snapshot + claimedBitmap := map[uint64]uint64{1: 5} + remainingTokens := uint64(1000) + id := manager.CreateSnapshot(claimedBitmap, remainingTokens) + + ufmt.Printf("Created snapshot with ID: %d\n", id) + + // Retrieve a snapshot + snap, err := manager.GetSnapshot(id) + if err != nil { + fmt.Printf("Error retrieving snapshot: %v\n", err) + return + } + + ufmt.Printf("Retrieved snapshot: ID=%d, Timestamp=%d, RemainingTokens=%d\n", + snap.ID, snap.Timestamp, snap.RemainingTokens) + + // Check a specific claim status + claimed, err := manager.IsClaimedAtSnapshot(id, 64) + if err != nil { + fmt.Printf("Error checking claim: %v\n", err) + return + } + + ufmt.Printf("Claim 64 is claimed: %v\n", claimed) +} +``` + +## License + +This package is licensed under the MIT License. See the [LICENSE](LICENSE) file for more information. + +[^1]: The bitmap is represented as a map of `uint64` value, where each bit represents a state of specific claim. From 848715285b452db33c7afe9f92eb7417db1f6403 Mon Sep 17 00:00:00 2001 From: Lee ByeongJun Date: Tue, 6 Aug 2024 14:47:11 +0900 Subject: [PATCH 08/10] fix example --- snapshot/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/snapshot/README.md b/snapshot/README.md index bc3e983..24331e0 100644 --- a/snapshot/README.md +++ b/snapshot/README.md @@ -35,7 +35,7 @@ func main() { remainingTokens := uint64(1000) id := manager.CreateSnapshot(claimedBitmap, remainingTokens) - ufmt.Printf("Created snapshot with ID: %d\n", id) + println(ufmt.Sprintf("Created snapshot with ID: %d\n", id)) // Retrieve a snapshot snap, err := manager.GetSnapshot(id) @@ -44,8 +44,9 @@ func main() { return } - ufmt.Printf("Retrieved snapshot: ID=%d, Timestamp=%d, RemainingTokens=%d\n", + res := ufmt.Sprintf("Retrieved snapshot: ID=%d, Timestamp=%d, RemainingTokens=%d\n", snap.ID, snap.Timestamp, snap.RemainingTokens) + println(res) // Check a specific claim status claimed, err := manager.IsClaimedAtSnapshot(id, 64) @@ -54,7 +55,7 @@ func main() { return } - ufmt.Printf("Claim 64 is claimed: %v\n", claimed) + println(ufmt.Printf("Claim 64 is claimed: %v\n", claimed)) } ``` From 029f047b24d5fdbe956f5fa7d2975ed926435480 Mon Sep 17 00:00:00 2001 From: Lee ByeongJun Date: Tue, 6 Aug 2024 18:51:14 +0900 Subject: [PATCH 09/10] remove std.Address from test --- airdrop/airdrop.gno | 2 +- airdrop/airdrop_test.gno | 23 ++++++++++------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/airdrop/airdrop.gno b/airdrop/airdrop.gno index 14f8167..685ccc3 100644 --- a/airdrop/airdrop.gno +++ b/airdrop/airdrop.gno @@ -39,7 +39,7 @@ type Config struct { type Airdrop struct { token grc20.Token config Config - claimedBitmap map[uint64]uint64 + claimedBitmap map[uint64]uint64 // claimID -> bitmap address std.Address snapshotManager *snapshot.Manager } diff --git a/airdrop/airdrop_test.gno b/airdrop/airdrop_test.gno index 96765ae..70cbef9 100644 --- a/airdrop/airdrop_test.gno +++ b/airdrop/airdrop_test.gno @@ -13,6 +13,8 @@ import ( "gno.land/p/demo/uint256" ) +var testAddress = testutils.TestAddress("test") + // mockGRC20 is a mock implementation of the IGRC20 interface for testing type mockGRC20 struct { balances map[std.Address]uint64 @@ -89,8 +91,7 @@ func (m *mockGRC20) TransferFrom(from, to std.Address, amount uint64) error { /////////////////////////////////////////////////////////////////////////////////////// func TestClaimIDToBitmapIndex(t *testing.T) { - address := std.Address("test") - a := NewAirdrop(nil, Config{}, address) + a := NewAirdrop(nil, Config{}, testAddress) tests := []struct { claimID uint64 @@ -114,8 +115,7 @@ func TestClaimIDToBitmapIndex(t *testing.T) { } func TestIsClaimed(t *testing.T) { - address := std.Address("test") - a := NewAirdrop(nil, Config{}, address) + a := NewAirdrop(nil, Config{}, testAddress) // Test unclaimed if a.IsClaimed(0) { @@ -137,10 +137,9 @@ func TestIsClaimed(t *testing.T) { } func TestClaim(t *testing.T) { - address := std.Address("test") mockToken := NewMockGRC20("Test Token", "TST", 18) - airdrop := NewAirdrop(mockToken, Config{}, address) + airdrop := NewAirdrop(mockToken, Config{}, testAddress) // Set up some initial balances airdropAddress := std.GetOrigCaller() @@ -199,11 +198,10 @@ func TestClaim(t *testing.T) { } func TestClaim64(t *testing.T) { - address := std.Address("test") mockToken := NewMockGRC20("Test Token", "TST", 18) // Create a new Airdrop instance - airdrop := NewAirdrop(mockToken, Config{}, address) + airdrop := NewAirdrop(mockToken, Config{}, testAddress) // Set up some initial balances airdropAddress := std.GetOrigCaller() @@ -293,8 +291,8 @@ func TestClaim64(t *testing.T) { func TestRefund(t *testing.T) { mockToken := NewMockGRC20("Test Token", "TST", 18) - refundTo := std.Address("refund_to") - airdropAddr := std.Address("airdrop_address") + refundTo := testutils.TestAddress("refund_to") + airdropAddr := testutils.TestAddress("airdrop") // Set up initial balances mockToken.balances[airdropAddr] = 1000000 @@ -375,10 +373,9 @@ func TestRefund(t *testing.T) { } func TestAirdropWithSnapshot(t *testing.T) { - address := std.Address("test_airdrop") mockToken := NewMockGRC20("Test Token", "TST", 18) - airdrop := NewAirdrop(mockToken, Config{}, address) + airdrop := NewAirdrop(mockToken, Config{}, testAddress) // Set up some initial balances airdropAddress := std.GetOrigCaller() @@ -389,7 +386,7 @@ func TestAirdropWithSnapshot(t *testing.T) { for i := 0; i < 128; i++ { claims[i] = Claim{ ID: uint64(i * 64), // Ensure first claim ID is multiple of 64 - Claimee: std.Address(ufmt.Sprintf("claimee%d", i)), + Claimee: testutils.TestAddress(ufmt.Sprintf("claimee%d", i)), Amount: uint256.NewUint(1000), // Each claim is for 1000 tokens } } From f624c242fdb67a39c3af7220066f667575372d45 Mon Sep 17 00:00:00 2001 From: Lee ByeongJun Date: Wed, 7 Aug 2024 10:13:16 +0900 Subject: [PATCH 10/10] fix: CreateSnapshot takes caller param --- airdrop/airdrop.gno | 5 +++-- airdrop/airdrop_test.gno | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/airdrop/airdrop.gno b/airdrop/airdrop.gno index 685ccc3..8a95f29 100644 --- a/airdrop/airdrop.gno +++ b/airdrop/airdrop.gno @@ -191,8 +191,9 @@ func (a *Airdrop) GetAddress() std.Address { } // CreateSnapshot creates a new snapshot of the current state -func (a *Airdrop) CreateSnapshot() uint64 { - return a.snapshotManager.CreateSnapshot(a.claimedBitmap, a.token.BalanceOf(std.GetOrigCaller())) +func (a *Airdrop) CreateSnapshot(caller string) uint64 { + callerAddr := std.Address(caller) + return a.snapshotManager.CreateSnapshot(a.claimedBitmap, a.token.BalanceOf(callerAddr)) } // IsClaimedAtSnapshot checks if a claim was made at a specific snapshot diff --git a/airdrop/airdrop_test.gno b/airdrop/airdrop_test.gno index 70cbef9..e13c5a3 100644 --- a/airdrop/airdrop_test.gno +++ b/airdrop/airdrop_test.gno @@ -398,7 +398,8 @@ func TestAirdropWithSnapshot(t *testing.T) { } // Create a snapshot after first batch - snapshotID := airdrop.CreateSnapshot() + caller := airdropAddress.String() + snapshotID := airdrop.CreateSnapshot(caller) // Process second batch of claims claimed, _ = airdrop.Claim64(claims[64:])