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

snapshot #8

Merged
merged 10 commits into from
Aug 7, 2024
Merged
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
36 changes: 28 additions & 8 deletions airdrop/airdrop.gno
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (

"gno.land/p/demo/grc/grc20"
"gno.land/p/demo/uint256"

"gno.land/r/governance/snapshot"
)

var (
Expand Down Expand Up @@ -35,19 +37,21 @@ type Config struct {

// Airdrop represents the airdrop contract
type Airdrop struct {
token grc20.Token
config Config
claimedBitmap map[uint64]uint64
address std.Address
token grc20.Token
config Config
claimedBitmap map[uint64]uint64 // claimID -> bitmap
address std.Address
snapshotManager *snapshot.Manager
}

// NewAirdrop creates a new Airdrop instance
func NewAirdrop(token grc20.Token, config Config, addr std.Address) *Airdrop {
return &Airdrop{
token: token,
config: config,
claimedBitmap: make(map[uint64]uint64),
address: addr,
token: token,
config: config,
claimedBitmap: make(map[uint64]uint64),
address: addr,
snapshotManager: snapshot.NewManager(),
}
}

Expand Down Expand Up @@ -185,3 +189,19 @@ func (a *Airdrop) GetConfig() Config {
func (a *Airdrop) GetAddress() std.Address {
return a.address
}

// CreateSnapshot creates a new snapshot of the current state
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
func (a *Airdrop) IsClaimedAtSnapshot(snapshotID uint64, claimID uint64) (bool, error) {
r3v4s marked this conversation as resolved.
Show resolved Hide resolved
return a.snapshotManager.IsClaimedAtSnapshot(snapshotID, claimID)
}

// GetSnapshot retrieves a specific snapshot
func (a *Airdrop) GetSnapshot(id uint64) (snapshot.Snapshot, error) {
return a.snapshotManager.GetSnapshot(id)
}
172 changes: 133 additions & 39 deletions airdrop/airdrop_test.gno
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -292,59 +290,155 @@ func TestClaim64(t *testing.T) {
}

func TestRefund(t *testing.T) {
token := NewMockGRC20("Test Token", "TST", 18)
mockToken := 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"))
airdropAddr := testutils.TestAddress("airdrop")

// Set up initial balances
mockToken.balances[airdropAddr] = 1000000

// Set some balance for the airdrop contract
token.balances[airdrop.address] = 1000
t.Run("Refund not allowed", func(t *testing.T) {
config := Config{
RefundableTimestamp: 0,
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 != ErrRefundNotAllowed {
t.Errorf("Expected ErrRefundNotAllowed, got %v", r)
}
}()
airdrop.Refund()
})

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)

defer func() {
if r := recover(); r == nil {
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])
}
})
}

func TestAirdropWithSnapshot(t *testing.T) {
mockToken := NewMockGRC20("Test Token", "TST", 18)

airdrop := NewAirdrop(mockToken, Config{}, testAddress)

// 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()
// })
// Set up some initial balances
airdropAddress := std.GetOrigCaller()
mockToken.balances[airdropAddress] = 1000000 // Initial balance of airdrop contract

// 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: testutils.TestAddress(ufmt.Sprintf("claimee%d", i)),
Amount: uint256.NewUint(1000), // Each claim is for 1000 tokens
}
}

// 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
caller := airdropAddress.String()
snapshotID := airdrop.CreateSnapshot(caller)

// Process second batch of claims
claimed, _ = airdrop.Claim64(claims[64:])
if claimed != 64 {
t.Errorf("Expected 64 claims, got %d", claimed)
}

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

// Test invalid snapshot ID
_, err := airdrop.IsClaimedAtSnapshot(snapshotID+1, 0)
if err == nil {
t.Error("Expected error for invalid snapshot ID, got nil")
}

// 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)
}
}
9 changes: 9 additions & 0 deletions airdrop/gno.mod
Original file line number Diff line number Diff line change
@@ -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
)
1 change: 0 additions & 1 deletion gno.mod

This file was deleted.

66 changes: 66 additions & 0 deletions snapshot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# 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)

println(ufmt.Sprintf("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
}

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)
if err != nil {
fmt.Printf("Error checking claim: %v\n", err)
return
}

println(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.
1 change: 1 addition & 0 deletions snapshot/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/r/governance/snapshot
Loading