diff --git a/launchpad/_helper_test.gno b/launchpad/_helper_test.gno index 46a22450..d1008f93 100644 --- a/launchpad/_helper_test.gno +++ b/launchpad/_helper_test.gno @@ -6,12 +6,18 @@ import ( "gno.land/p/demo/grc/grc20" "gno.land/p/demo/testutils" - "gno.land/p/demo/ufmt" pusers "gno.land/p/demo/users" "gno.land/r/demo/users" + "gno.land/r/gnoswap/v1/common" "gno.land/r/gnoswap/v1/consts" ) +var ( + adminAddr = consts.ADMIN + adminUser = common.AddrToUser(adminAddr) + adminRealm = std.NewUserRealm(adminAddr) +) + // MockXGNSToken implements basic functionality for testing xgns token type MockXGNSToken struct { token *grc20.Token diff --git a/launchpad/deposit.gno b/launchpad/deposit.gno index 3502c4dd..4b58a0ba 100644 --- a/launchpad/deposit.gno +++ b/launchpad/deposit.gno @@ -77,10 +77,10 @@ func isTierActive(tier Tier, height uint64) bool { // createDeposit creates a new deposit entry func createDeposit(info ProjectTierInfo, amount uint64) (Deposit, error) { depositor := std.PrevRealm().Addr() - depositId := ufmt.Sprintf("%s:%s:%s:%d", - info.Project.id, - info.TierStr, - depositor.String(), + depositId := ufmt.Sprintf("%s:%s:%s:%d", + info.Project.id, + info.TierStr, + depositor.String(), info.Height, ) @@ -127,7 +127,7 @@ func updateDepositIndices(deposit Deposit, state *DepositState) { state.DepositsByUser[deposit.depositor] = []string{} } state.DepositsByUser[deposit.depositor] = append( - state.DepositsByUser[deposit.depositor], + state.DepositsByUser[deposit.depositor], deposit.id, ) @@ -139,7 +139,7 @@ func updateDepositIndices(deposit Deposit, state *DepositState) { state.DepositsByProject[deposit.projectId][deposit.tier] = []string{} } state.DepositsByProject[deposit.projectId][deposit.tier] = append( - state.DepositsByProject[deposit.projectId][deposit.tier], + state.DepositsByProject[deposit.projectId][deposit.tier], deposit.id, ) @@ -151,7 +151,7 @@ func updateDepositIndices(deposit Deposit, state *DepositState) { state.DepositsByUserProject[deposit.depositor][deposit.projectId] = []string{} } state.DepositsByUserProject[deposit.depositor][deposit.projectId] = append( - state.DepositsByUserProject[deposit.depositor][deposit.projectId], + state.DepositsByUserProject[deposit.depositor][deposit.projectId], deposit.id, ) } @@ -188,7 +188,7 @@ func DepositGns(targetProjectTierId string, amount uint64) string { en.MintAndDistributeGns() - project = projects[projectId] // get updates project + project = projects[projectId] // get updates project tier = getTier(project, tierStr) // get updates tier // Update gov_staker to calculate project's recipient's reward @@ -289,7 +289,7 @@ func processDepositCollection( } tier := getTier(project, dpst.tier) - if isTierActive(tier, height) || dpst.depositCollectHeight != 0 { + if !isPassedClaimableHeight(dpst, height) || dpst.depositCollectHeight != 0 { continue } @@ -372,94 +372,94 @@ func processCollectedDeposits(dpsts []string, pid string) uint64 { } func validateDepositCollection(depositId string, caller std.Address) (Deposit, Project, error) { - deposit, exists := deposits[depositId] - if !exists { - return Deposit{}, Project{}, errors.New("deposit not found") - } + deposit, exists := deposits[depositId] + if !exists { + return Deposit{}, Project{}, errors.New("deposit not found") + } - project, exists := projects[deposit.projectId] - if !exists { - return Deposit{}, Project{}, errors.New("project not found") - } + project, exists := projects[deposit.projectId] + if !exists { + return Deposit{}, Project{}, errors.New("project not found") + } - if _, exists := depositsByUserByProject[caller]; !exists { - return Deposit{}, Project{}, errors.New("no deposits for this user") - } + if _, exists := depositsByUserByProject[caller]; !exists { + return Deposit{}, Project{}, errors.New("no deposits for this user") + } - return deposit, project, nil + return deposit, project, nil } // receive deposit and project as an pointer to avoid copying func processDeposit(deposit *Deposit, project *Project, height uint64) (uint64, error) { - tier := getTier(*project, deposit.tier) - if isTierActive(tier, height) { - return 0, errors.New("tier is still active") - } - - if deposit.depositCollectHeight != 0 { - return 0, errors.New("deposit already collected") - } - - // Update deposit status - deposit.depositCollectHeight = height - deposit.depositCollectTime = uint64(time.Now().Unix()) - deposits[deposit.id] = *deposit - - // Update project tier - tier.actualDepositAmount -= deposit.amount - tier.actualParticipant-- - - // Update project - *project = setTier(*project, deposit.tier, tier) - project.stats.actualDeposit -= deposit.amount - project.stats.actualParticipant-- - projects[deposit.projectId] = *project - - return deposit.amount, nil + tier := getTier(*project, deposit.tier) + if isTierActive(tier, height) { + return 0, errors.New("tier is still active") + } + + if deposit.depositCollectHeight != 0 { + return 0, errors.New("deposit already collected") + } + + // Update deposit status + deposit.depositCollectHeight = height + deposit.depositCollectTime = uint64(time.Now().Unix()) + deposits[deposit.id] = *deposit + + // Update project tier + tier.actualDepositAmount -= deposit.amount + tier.actualParticipant-- + + // Update project + *project = setTier(*project, deposit.tier, tier) + project.stats.actualDeposit -= deposit.amount + project.stats.actualParticipant-- + projects[deposit.projectId] = *project + + return deposit.amount, nil } func CollectDepositGnsByDepositId(depositId string) uint64 { common.IsHalted() - caller := std.PrevRealm().Addr() - - // Validate deposit collection request - deposit, project, err := validateDepositCollection(depositId, caller) - if err != nil { - return 0 - } - - en.MintAndDistributeGns() - - project = projects[deposit.projectId] // get updated project - - // Process deposit collection - height := uint64(std.GetHeight()) - amount, err := processDeposit(&deposit, &project, height) - if err != nil { - return 0 - } - - // Update gov_staker contract - gs.SetAmountByProjectWallet(project.recipient, amount, false) - - // Emit collection event - prevAddr, prevRealm := getPrev() - std.Emit( - "CollectDepositGnsByDepositId", - "prevAddr", prevAddr, - "prevRealm", prevRealm, - "depositId", depositId, - "amount", ufmt.Sprintf("%d", amount), - ) - - // Process token transfers if amount > 0 - if amount > 0 { - xgns.Burn(a2u(consts.LAUNCHPAD_ADDR), amount) - gns.Transfer(a2u(caller), amount) - return amount - } - - return 0 + caller := std.PrevRealm().Addr() + + // Validate deposit collection request + deposit, project, err := validateDepositCollection(depositId, caller) + if err != nil { + return 0 + } + + en.MintAndDistributeGns() + + project = projects[deposit.projectId] // get updated project + + // Process deposit collection + height := uint64(std.GetHeight()) + amount, err := processDeposit(&deposit, &project, height) + if err != nil { + return 0 + } + + // Update gov_staker contract + gs.SetAmountByProjectWallet(project.recipient, amount, false) + + // Emit collection event + prevAddr, prevRealm := getPrev() + std.Emit( + "CollectDepositGnsByDepositId", + "prevAddr", prevAddr, + "prevRealm", prevRealm, + "depositId", depositId, + "amount", ufmt.Sprintf("%d", amount), + ) + + // Process token transfers if amount > 0 + if amount > 0 { + xgns.Burn(a2u(consts.LAUNCHPAD_ADDR), amount) + gns.Transfer(a2u(caller), amount) + return amount + } + + return 0 } func getProjectIdFromTierId(tierId string) string { @@ -499,7 +499,7 @@ func checkDepositConditions(project Project) { if condition.minAmount == 0 { continue } - + // check balance var balance uint64 if condition.tokenPath == consts.GOV_XGNS_PATH { @@ -573,3 +573,7 @@ func setTier(project Project, tierStr string, tier Tier) Project { return project } + +func isPassedClaimableHeight(deposit Deposit, height uint64) bool { + return deposit.claimableHeight <= height +} diff --git a/launchpad/deposit_test.gno b/launchpad/deposit_test.gno index 08a2679a..00cc056e 100644 --- a/launchpad/deposit_test.gno +++ b/launchpad/deposit_test.gno @@ -2,16 +2,17 @@ package launchpad import ( "std" - "strings" "testing" "time" "gno.land/p/demo/testutils" "gno.land/p/demo/uassert" "gno.land/p/demo/ufmt" + "gno.land/r/gnoswap/v1/common" "gno.land/r/gnoswap/v1/consts" - "gno.land/r/gnoswap/v1/gov/xgns" + + "gno.land/r/gnoswap/v1/gns" ) // Mock data structs @@ -320,12 +321,11 @@ func TestDepositGns(t *testing.T) { t.Run("Fail with insufficient balance", func(t *testing.T) { std.TestSetRealm(std.NewUserRealm(testAddr)) - mockTeller.balance = 500 // Less than required deposit amount defer func() { r := recover() if r != nil { - t.Fatalf("Recovered from panic: %v", r) + t.Logf("Recovered from panic: %v", r) } }() @@ -333,8 +333,12 @@ func TestDepositGns(t *testing.T) { }) t.Run("Success with sufficient balance", func(t *testing.T) { + // transfer enough gns + std.TestSetRealm(adminRealm) + gns.Transfer(common.AddrToUser(testAddr), 1000) + std.TestSetRealm(std.NewUserRealm(testAddr)) - mockTeller.balance = 2000 // More than required deposit amount + gns.Approve(common.AddrToUser(consts.LAUNCHPAD_ADDR), 1000) depositId := DepositGns(project.id+":30", 1000) deposit := deposits[depositId] @@ -403,6 +407,7 @@ func TestMultipleDeposits(t *testing.T) { CurrentTime: uint64(time.Now().Unix()), } + std.TestSetRealm(std.NewUserRealm(testAddr)) deposit, _ := createDeposit(info, 100) deposits[deposit.id] = deposit updateDepositIndices(deposit, state) diff --git a/launchpad/reward_test.gno b/launchpad/reward_test.gno index e75405b6..1b731c6d 100644 --- a/launchpad/reward_test.gno +++ b/launchpad/reward_test.gno @@ -3,8 +3,8 @@ package launchpad import ( "testing" - "gno.land/p/demo/ufmt" "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" u256 "gno.land/p/gnoswap/uint256" ) @@ -21,28 +21,16 @@ func TestValidateRewardCollection(t *testing.T) { { name: "Valid reward ready to collect", deposit: Deposit{ - //rewardAmount: 1000, - claimableHeight: 50, // Less than current height + claimableHeight: 50, // Less than current height rewardCollectTime: 0, }, height: currentHeight, shouldError: false, }, - { - name: "No reward available", - deposit: Deposit{ - //rewardAmount: 0, - claimableHeight: 50, - }, - height: currentHeight, - shouldError: true, - errorMsg: "no reward available", - }, { name: "Reward not yet claimable", deposit: Deposit{ - //rewardAmount: 1000, - claimableHeight: 150, // Greater than current height + claimableHeight: 150, // Greater than current height rewardCollectTime: 0, }, height: currentHeight, @@ -52,7 +40,6 @@ func TestValidateRewardCollection(t *testing.T) { { name: "Already collected reward can be collected before claimable height", deposit: Deposit{ - //rewardAmount: 1000, claimableHeight: 150, rewardCollectTime: 1, // Already collected before }, @@ -118,125 +105,125 @@ func TestCalculateDepositRatioX96(t *testing.T) { } func TestProcessDepositReward(t *testing.T) { - q96 = u256.MustFromDecimal("79228162514264337593543950336") - - tests := []struct { - name string - deposit Deposit - rewardX96 *u256.Uint - tierAmount uint64 - expectedReward uint64 - shouldError bool - }{ - { - name: "Normal reward calculation", - deposit: Deposit{ - amount: 1000, - //rewardAmount: 0, - }, - rewardX96: u256.NewUint(1000).Mul(u256.NewUint(1000), q96), - tierAmount: 2000, - expectedReward: 500, // (1000/2000) * 1000 = 500 - shouldError: false, - }, + q96 = u256.MustFromDecimal("79228162514264337593543950336") + + tests := []struct { + name string + deposit Deposit + rewardX96 *u256.Uint + tierAmount uint64 + expectedReward uint64 + shouldError bool + }{ + { + name: "Normal reward calculation", + deposit: Deposit{ + amount: 1000, + //rewardAmount: 0, + }, + rewardX96: u256.NewUint(1000).Mul(u256.NewUint(1000), q96), + tierAmount: 2000, + expectedReward: 500, // (1000/2000) * 1000 = 500 + shouldError: false, + }, /* - { - name: "Tier amount is 0", - deposit: Deposit{ - amount: 1000, - //rewardAmount: 0, - }, - rewardX96: u256.NewUint(1000), - tierAmount: 0, - expectedReward: 0, - shouldError: true, - },*/ - { - name: "Reward is accumulated", - deposit: Deposit{ - amount: 1000, - //rewardAmount: 100, - }, - rewardX96: u256.NewUint(1000).Mul(u256.NewUint(1000), q96), - tierAmount: 2000, - expectedReward: 500, // 600, // Existing 100 + New reward 500 - shouldError: false, - }, - } - - for i, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + { + name: "Tier amount is 0", + deposit: Deposit{ + amount: 1000, + //rewardAmount: 0, + }, + rewardX96: u256.NewUint(1000), + tierAmount: 0, + expectedReward: 0, + shouldError: true, + },*/ + { + name: "Reward is accumulated", + deposit: Deposit{ + amount: 1000, + //rewardAmount: 100, + }, + rewardX96: u256.NewUint(1000).Mul(u256.NewUint(1000), q96), + tierAmount: 2000, + expectedReward: 500, // 600, // Existing 100 + New reward 500 + shouldError: false, + }, + } + + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { state := NewRewardState(tt.rewardX96, 1000, 2000) depositId := ufmt.Sprintf("depositId-%d", i) state.AddStake(1000, depositId, tt.deposit.amount) state.TotalStake = tt.tierAmount // force overriding total stake state.finalize(1001) - reward := state.CalculateReward(depositId) - uassert.Equal(t, tt.expectedReward, reward) - }) - } + reward := state.CalculateReward(depositId) + uassert.Equal(t, tt.expectedReward, reward) + }) + } } func TestCalculateTierRewards(t *testing.T) { - q96 = u256.MustFromDecimal("79228162514264337593543950336") - - tests := []struct { - name string - tier Tier - currentHeight uint64 - lastCalcHeight uint64 - expectedRewardX96 string - expectedReward uint64 - shouldBeZero bool - }{ - { - name: "tierAmountPerBlockX96 is 0", - tier: Tier{ - tierAmountPerBlockX96: u256.Zero(), - ended: TimeInfo{height: 1000}, - }, - currentHeight: 500, - lastCalcHeight: 400, - shouldBeZero: true, - }, - { - name: "sinceLast is 0", - tier: Tier{ - tierAmountPerBlockX96: u256.NewUint(1000), - ended: TimeInfo{height: 1000}, - }, - currentHeight: 500, - lastCalcHeight: 500, - shouldBeZero: true, - }, - { - name: "Current height exceeds end height", - tier: Tier{ - tierAmountPerBlockX96: u256.NewUint(1000), - ended: TimeInfo{height: 450}, - }, - currentHeight: 500, - lastCalcHeight: 400, - expectedRewardX96: "50000", // 1000 * (450-400) - expectedReward: 0, // (50000 / Q96) - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - rewardX96, reward, err := calculateTierRewards(tt.tier, tt.currentHeight, tt.lastCalcHeight) - - uassert.NoError(t, err) - - if tt.shouldBeZero { - uassert.Equal(t, true, rewardX96.IsZero()) - uassert.Equal(t, uint64(0), reward) - } else { - expected := u256.MustFromDecimal(tt.expectedRewardX96) - uassert.Equal(t, 0, rewardX96.Cmp(expected)) - uassert.Equal(t, tt.expectedReward, reward) - } - }) - } + q96 = u256.MustFromDecimal("79228162514264337593543950336") + + tests := []struct { + name string + tier Tier + currentHeight uint64 + lastCalcHeight uint64 + expectedRewardX96 string + expectedReward uint64 + shouldBeZero bool + }{ + { + name: "tierAmountPerBlockX96 is 0", + tier: Tier{ + tierAmountPerBlockX96: u256.Zero(), + ended: TimeInfo{height: 1000}, + }, + currentHeight: 500, + lastCalcHeight: 400, + shouldBeZero: true, + }, + { + name: "sinceLast is 0", + tier: Tier{ + tierAmountPerBlockX96: u256.NewUint(1000), + ended: TimeInfo{height: 1000}, + }, + currentHeight: 500, + lastCalcHeight: 500, + shouldBeZero: true, + }, + { + name: "Current height exceeds end height", + tier: Tier{ + tierAmountPerBlockX96: u256.NewUint(1000), + ended: TimeInfo{height: 450}, + }, + currentHeight: 500, + lastCalcHeight: 400, + expectedRewardX96: "50000", // 1000 * (450-400) + expectedReward: 0, // (50000 / Q96) + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rewardX96, reward, err := calculateTierRewards(tt.tier, tt.currentHeight, tt.lastCalcHeight) + + uassert.NoError(t, err) + + if tt.shouldBeZero { + uassert.Equal(t, true, rewardX96.IsZero()) + uassert.Equal(t, uint64(0), reward) + } else { + expected := u256.MustFromDecimal(tt.expectedRewardX96) + uassert.Equal(t, 0, rewardX96.Cmp(expected)) + uassert.Equal(t, tt.expectedReward, reward) + } + }) + } }