diff --git a/staker/reward_internal_emission_test.gno b/staker/reward_internal_emission_test.gno new file mode 100644 index 00000000..b966ca88 --- /dev/null +++ b/staker/reward_internal_emission_test.gno @@ -0,0 +1,158 @@ +package staker + +import ( + "testing" + + u256 "gno.land/p/gnoswap/uint256" +) + +func TestRemoveInRangePosition(t *testing.T) { + tests := []struct { + name string + poolPath string + tokenId uint64 + setup func() *InternalEmissionReward + verify func(*InternalEmissionReward) + }{ + { + name: "Normal position removal", + poolPath: "test/pool/path", + tokenId: 1, + setup: func() *InternalEmissionReward { + r := NewInternalEmissionReward() + recipientsMap := NewRewardRecipientMap() + poolLiquidity := NewPoolLiquidity() + + // Initial liquidity setup + inRangeLiquidity := NewInRangeLiquidity() + inRangeLiquidity.SetLiquidity(u256.NewUint(1000)) + poolLiquidity.AddInRangePosition(1, inRangeLiquidity) + + recipientsMap.SetPoolLiquidity("test/pool/path", poolLiquidity) + r.SetRewardRecipientsMap(recipientsMap) + return r + }, + verify: func(r *InternalEmissionReward) { + recipientsMap := r.GetRewardRecipientsMap() + poolLiquidity := recipientsMap.GetPoolLiquidity("test/pool/path") + + // Check if position is removed + position := poolLiquidity.GetInRangeLiquidity(1) + if !position.GetLiquidity().IsZero() { + t.Errorf("Liquidity should be 0, but it is %s", position.GetLiquidity().ToString()) + } + if !position.GetLiquidityRatio().IsZero() { + t.Errorf("Liquidity ratio should be 0, but it is %s", position.GetLiquidityRatio().ToString()) + } + if position.GetStakedHeight() != 0 { + t.Errorf("Staking height should be 0, but it is %d", position.GetStakedHeight()) + } + }, + }, + { + name: "Non-existent pool path", + poolPath: "non/existent/path", + tokenId: 1, + setup: func() *InternalEmissionReward { + return NewInternalEmissionReward() + }, + verify: func(r *InternalEmissionReward) { + // No Error + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := tt.setup() + r.RemoveInRangePosition(tt.poolPath, tt.tokenId) + tt.verify(r) + }) + } +} + +func TestSelectRewardPools(t *testing.T) { + tests := []struct { + name string + pools map[string]InternalTier + setup func() *InternalEmissionReward + verify func(*InternalEmissionReward) + }{ + { + name: "Normal pool selection", + pools: map[string]InternalTier{ + "pool/1": {tier: TIER1_INDEX}, + "pool/2": {tier: TIER2_INDEX}, + "pool/3": {tier: TIER3_INDEX}, + "pool/4": {tier: 4}, // Invalid tier + }, + setup: func() *InternalEmissionReward { + return NewInternalEmissionReward() + }, + verify: func(r *InternalEmissionReward) { + rewardPool := r.GetRewardPoolsMap() + + // TIER1 pool verification + pool1 := rewardPool.GetRewardPoolByPoolPath("pool/1") + if pool1.GetTier() != TIER1_INDEX { + t.Errorf("pool/1 should be TIER1, but got %d", pool1.GetTier()) + } + + // TIER2 pool verification + pool2 := rewardPool.GetRewardPoolByPoolPath("pool/2") + if pool2.GetTier() != TIER2_INDEX { + t.Errorf("pool/2 should be TIER2, but got %d", pool2.GetTier()) + } + + // TIER3 pool verification + pool3 := rewardPool.GetRewardPoolByPoolPath("pool/3") + if pool3.GetTier() != TIER3_INDEX { + t.Errorf("pool/3 should be TIER3, but got %d", pool3.GetTier()) + } + + // Invalid tier pool verification + pool4 := rewardPool.GetRewardPoolByPoolPath("pool/4") + if pool4.GetTier() != 0 { + t.Errorf("pool/4 should not be selected, but got tier %d", pool4.GetTier()) + } + }, + }, + { + name: "Empty pool map", + pools: map[string]InternalTier{}, + setup: func() *InternalEmissionReward { + return NewInternalEmissionReward() + }, + verify: func(r *InternalEmissionReward) { + rewardPool := r.GetRewardPoolsMap() + if len(rewardPool.GetRewardPools()) != 0 { + t.Errorf("reward pools should be empty, but got %d pools", len(rewardPool.GetRewardPools())) + } + }, + }, + { + name: "Invalid tier only", + pools: map[string]InternalTier{ + "pool/1": {tier: 4}, + "pool/2": {tier: 5}, + }, + setup: func() *InternalEmissionReward { + return NewInternalEmissionReward() + }, + verify: func(r *InternalEmissionReward) { + rewardPool := r.GetRewardPoolsMap() + if len(rewardPool.GetRewardPools()) != 0 { + t.Errorf("no pools should be selected, but got %d pools", len(rewardPool.GetRewardPools())) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := tt.setup() + r.SelectRewardPools(tt.pools) + tt.verify(r) + }) + } +} diff --git a/staker/reward_manager_test.gno b/staker/reward_manager_test.gno new file mode 100644 index 00000000..9817a9da --- /dev/null +++ b/staker/reward_manager_test.gno @@ -0,0 +1,191 @@ +package staker + +import ( + "testing" + + u256 "gno.land/p/gnoswap/uint256" +) + +func TestRewardManager_Internal(t *testing.T) { + tests := []struct { + name string + setup func() *RewardManager + testFunc func(*RewardManager) + verify func(*RewardManager) bool + wantErr bool + }{ + { + name: "create new RewardManager instance", + setup: func() *RewardManager { + return NewRewardManager() + }, + verify: func(rm *RewardManager) bool { + return rm.internalReward != nil && rm.externalReward != nil + }, + }, + { + name: "set and get InternalEmissionReward", + setup: func() *RewardManager { + return NewRewardManager() + }, + testFunc: func(rm *RewardManager) { + newInternalReward := NewInternalEmissionReward() + recipientsMap := NewRewardRecipientMap() + poolLiquidity := NewPoolLiquidity() + inRangeLiquidity := NewInRangeLiquidity() + inRangeLiquidity.SetLiquidity(u256.NewUint(1000)) + poolLiquidity.AddInRangePosition(1, inRangeLiquidity) + recipientsMap.SetPoolLiquidity("test/pool", poolLiquidity) + newInternalReward.SetRewardRecipientsMap(recipientsMap) + + rm.SetInternalEmissionReward(newInternalReward) + }, + verify: func(rm *RewardManager) bool { + internalReward := rm.GetInternalEmissionReward() + if internalReward == nil { + return false + } + + recipientsMap := internalReward.GetRewardRecipientsMap() + if recipientsMap == nil { + return false + } + + poolLiquidity := recipientsMap.GetPoolLiquidity("test/pool") + if poolLiquidity == nil { + return false + } + + position := poolLiquidity.GetInRangeLiquidity(1) + if position == nil { + return false + } + + return position.GetLiquidity().Eq(u256.NewUint(1000)) + }, + }, + { + name: "set nil InternalEmissionReward", + setup: func() *RewardManager { + return NewRewardManager() + }, + testFunc: func(rm *RewardManager) { + rm.SetInternalEmissionReward(nil) + }, + verify: func(rm *RewardManager) bool { + return rm.GetInternalEmissionReward() == nil + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rm := tt.setup() + + if tt.testFunc != nil { + tt.testFunc(rm) + } + + if result := tt.verify(rm); !result { + t.Errorf("%s: verification failed", tt.name) + } + }) + } +} + +func TestRewardManager_External(t *testing.T) { + tests := []struct { + name string + setup func() *RewardManager + testFunc func(*RewardManager) + verify func(*RewardManager) bool + wantErr bool + }{ + { + name: "create new RewardManager instance for external reward", + setup: func() *RewardManager { + return NewRewardManager() + }, + verify: func(rm *RewardManager) bool { + return rm.externalReward != nil + }, + }, + { + name: "set and get ExternalIncentiveReward", + setup: func() *RewardManager { + return NewRewardManager() + }, + testFunc: func(rm *RewardManager) { + newExternalReward := NewExternalIncentiveReward() + externalCalculator := NewExternalCalculator(100) + newExternalReward.SetExternalCalculator(externalCalculator) + + rm.SetExternalIncentiveReward(newExternalReward) + }, + verify: func(rm *RewardManager) bool { + externalReward := rm.GetExternalIncentiveReward() + if externalReward == nil { + return false + } + + calculator := externalReward.GetExternalCalculator() + if calculator == nil { + return false + } + + return true + }, + }, + { + name: "set nil ExternalIncentiveReward", + setup: func() *RewardManager { + return NewRewardManager() + }, + testFunc: func(rm *RewardManager) { + rm.SetExternalIncentiveReward(nil) + }, + verify: func(rm *RewardManager) bool { + return rm.GetExternalIncentiveReward() == nil + }, + }, + { + name: "get or create ExternalCalculator", + setup: func() *RewardManager { + rm := NewRewardManager() + newExternalReward := NewExternalIncentiveReward() + rm.SetExternalIncentiveReward(newExternalReward) + return rm + }, + testFunc: func(rm *RewardManager) { + externalReward := rm.GetExternalIncentiveReward() + calculator := externalReward.GetOrCreateExternalCalculator(200) // 높이 200으로 설정 + if calculator == nil { + t.Error("calculator should not be nil") + } + }, + verify: func(rm *RewardManager) bool { + externalReward := rm.GetExternalIncentiveReward() + if externalReward == nil { + return false + } + + calculator := externalReward.GetExternalCalculator() + return calculator != nil + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rm := tt.setup() + + if tt.testFunc != nil { + tt.testFunc(rm) + } + + if result := tt.verify(rm); !result { + t.Errorf("%s: verification failed", tt.name) + } + }) + } +} diff --git a/staker/reward_recipient_store.gno b/staker/reward_recipient_store.gno index ceda2e4b..3fe77a3c 100644 --- a/staker/reward_recipient_store.gno +++ b/staker/reward_recipient_store.gno @@ -172,7 +172,7 @@ func (r *RewardRecipientsMap) CalculateLiquidityRatioAndGetTokenIdMap() map[stri // 2. calculate the liquidity ratio for each position for poolPath, poolLiquidity := range poolLiquidityMap { inRangePosition := poolLiquidity.GetInRangeLiquidityMap() - for tokenId, _ := range inRangePosition { + for tokenId := range inRangePosition { r.UpdateInRangeLiquidityRatio(poolPath, tokenId) tokenIdMap[poolPath] = append(tokenIdMap[poolPath], tokenId) } @@ -195,6 +195,33 @@ func NewPoolLiquidity() *PoolLiquidity { } } +// AddInRangePosition adds a new in-range position to the pool +func (p *PoolLiquidity) AddInRangePosition(tokenID uint64, position *InRangeLiquidity) { + // nil check for map + if p.inRangeLiquidityMap == nil { + p.inRangeLiquidityMap = make(map[uint64]*InRangeLiquidity) + } + + // nil check for position + if position == nil { + position = NewInRangeLiquidity() + } + + // nil check for totalLiquidity + if p.totalLiquidity == nil { + p.totalLiquidity = u256.Zero() + } + + // Add position's liquidity to total liquidity + positionLiquidity := position.GetLiquidity() + if positionLiquidity != nil { + p.totalLiquidity.Add(p.totalLiquidity, positionLiquidity) + } + + // Store the position + p.inRangeLiquidityMap[tokenID] = position +} + func (p *PoolLiquidity) SetTotalLiquidity(totalLiquidity *u256.Uint) { p.totalLiquidity = totalLiquidity } diff --git a/staker/reward_recipient_store_test.gno b/staker/reward_recipient_store_test.gno new file mode 100644 index 00000000..7adb71d4 --- /dev/null +++ b/staker/reward_recipient_store_test.gno @@ -0,0 +1,160 @@ +package staker + +import ( + "testing" + + u256 "gno.land/p/gnoswap/uint256" +) + +func TestAddInRangePosition(t *testing.T) { + tests := []struct { + name string + tokenID uint64 + setup func() (*PoolLiquidity, *InRangeLiquidity) + verify func(*PoolLiquidity) + }{ + { + name: "Normal position addition", + tokenID: 1, + setup: func() (*PoolLiquidity, *InRangeLiquidity) { + poolLiquidity := NewPoolLiquidity() + position := NewInRangeLiquidity() + position.SetLiquidity(u256.NewUint(1000)) + position.SetLiquidityRatio(u256.NewUint(100)) + position.SetStakedHeight(50) + return poolLiquidity, position + }, + verify: func(p *PoolLiquidity) { + checkIfMapIsInitialized(t, p.inRangeLiquidityMap) + + position := p.GetInRangeLiquidity(1) + checkIfPositionIsStoredCorrectly(t, position) + checkIfLiquidityIsStoredCorrectly(t, position, u256.NewUint(1000)) + checkIfTotalLiquidityIsStoredCorrectly(t, p, u256.NewUint(1000)) + }, + }, + { + name: "nil position addition", + tokenID: 2, + setup: func() (*PoolLiquidity, *InRangeLiquidity) { + poolLiquidity := NewPoolLiquidity() + return poolLiquidity, nil + }, + verify: func(p *PoolLiquidity) { + checkIfMapIsInitialized(t, p.inRangeLiquidityMap) + + position := p.GetInRangeLiquidity(2) + checkIfPositionIsStoredCorrectly(t, position) + + if !position.GetLiquidity().IsZero() { + t.Errorf("liquidity should be zero, got %s", position.GetLiquidity().ToString()) + } + }, + }, + { + name: "nil map addition", + tokenID: 3, + setup: func() (*PoolLiquidity, *InRangeLiquidity) { + poolLiquidity := &PoolLiquidity{ + inRangeLiquidityMap: nil, + totalLiquidity: nil, + } + position := NewInRangeLiquidity() + position.SetLiquidity(u256.NewUint(500)) + return poolLiquidity, position + }, + verify: func(p *PoolLiquidity) { + checkIfMapIsInitialized(t, p.inRangeLiquidityMap) + + position := p.GetInRangeLiquidity(3) + checkIfPositionIsStoredCorrectly(t, position) + checkIfLiquidityIsStoredCorrectly(t, position, u256.NewUint(500)) + checkIfTotalLiquidityIsStoredCorrectly(t, p, u256.NewUint(500)) + }, + }, + { + name: "Multiple position addition", + tokenID: 4, + setup: func() (*PoolLiquidity, *InRangeLiquidity) { + poolLiquidity := NewPoolLiquidity() + + // first position addition + position1 := NewInRangeLiquidity() + position1.SetLiquidity(u256.NewUint(300)) + poolLiquidity.AddInRangePosition(1, position1) + + // second position addition (test position) + position2 := NewInRangeLiquidity() + position2.SetLiquidity(u256.NewUint(700)) + + return poolLiquidity, position2 + }, + verify: func(p *PoolLiquidity) { + // check if total liquidity is stored correctly (300 + 700 = 1000) + if !p.GetTotalLiquidity().Eq(u256.NewUint(1000)) { + t.Errorf("expected total liquidity 1000, got %s", p.GetTotalLiquidity().ToString()) + } + + // check if each position is stored correctly + position1 := p.GetInRangeLiquidity(1) + checkIfLiquidityIsStoredCorrectly(t, position1, u256.NewUint(300)) + + position2 := p.GetInRangeLiquidity(4) + checkIfLiquidityIsStoredCorrectly(t, position2, u256.NewUint(700)) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + poolLiquidity, position := tt.setup() + poolLiquidity.AddInRangePosition(tt.tokenID, position) + tt.verify(poolLiquidity) + }) + } +} + +func checkIfMapIsInitialized( + t *testing.T, + targetMap map[uint64]*InRangeLiquidity, +) { + t.Helper() + if targetMap == nil { + t.Error("targetMap should be initialized") + } +} + +func checkIfPositionIsStoredCorrectly( + t *testing.T, + targetPosition *InRangeLiquidity, +) { + t.Helper() + + if targetPosition == nil { + t.Error("targetPosition should not be nil") + } +} + +func checkIfLiquidityIsStoredCorrectly( + t *testing.T, + position *InRangeLiquidity, + expectedLiquidity *u256.Uint, +) { + t.Helper() + + if !position.GetLiquidity().Eq(expectedLiquidity) { + t.Errorf("expected liquidity %s, got %s", expectedLiquidity.ToString(), position.GetLiquidity().ToString()) + } +} + +func checkIfTotalLiquidityIsStoredCorrectly( + t *testing.T, + poolLiquidity *PoolLiquidity, + expectedTotalLiquidity *u256.Uint, +) { + t.Helper() + + if !poolLiquidity.GetTotalLiquidity().Eq(expectedTotalLiquidity) { + t.Errorf("expected total liquidity %s, got %s", expectedTotalLiquidity.ToString(), poolLiquidity.GetTotalLiquidity().ToString()) + } +} diff --git a/staker/staker.gno b/staker/staker.gno index 64e0a747..7b27bfe7 100644 --- a/staker/staker.gno +++ b/staker/staker.gno @@ -40,13 +40,13 @@ func init() { } type stakeResult struct { - tokenId uint64 - owner std.Address - caller std.Address - poolPath string + tokenId uint64 + owner std.Address + caller std.Address + poolPath string token0Amount string token1Amount string - deposit Deposit + deposit Deposit } func calculateStakeData(tokenId uint64, owner, caller std.Address) (*stakeResult, error) { @@ -70,7 +70,7 @@ func calculateStakeData(tokenId uint64, owner, caller std.Address) (*stakeResult newDeposit := newDeposit( owner, - deposit.NumberOfStakes() + 1, + deposit.NumberOfStakes()+1, time.Now().Unix(), std.GetHeight(), poolPath, @@ -79,13 +79,13 @@ func calculateStakeData(tokenId uint64, owner, caller std.Address) (*stakeResult token0Amount, token1Amount := getTokenPairBalanceFromPosition(tokenId) return &stakeResult{ - tokenId: tokenId, - owner: owner, - caller: caller, - poolPath: poolPath, + tokenId: tokenId, + owner: owner, + caller: caller, + poolPath: poolPath, token0Amount: token0Amount, token1Amount: token1Amount, - deposit: newDeposit, + deposit: newDeposit, }, nil } @@ -162,25 +162,25 @@ func transferDeposit(tokenId uint64, owner, caller, to std.Address) error { //////////////////////////////////////////////////////////// type collectResult struct { - tokenId uint64 - owner std.Address - poolPath string + tokenId uint64 + owner std.Address + poolPath string internalRewards warmUpAmount externalRewards *avl.Tree } type externalRewardInfo struct { - ictvId string - tokenPath string + ictvId string + tokenPath string fullAmount uint64 - toGive uint64 + toGive uint64 } func newCollectResult(tokenId uint64, owner std.Address, poolPath string) *collectResult { return &collectResult{ - tokenId: tokenId, - owner: owner, - poolPath: poolPath, + tokenId: tokenId, + owner: owner, + poolPath: poolPath, internalRewards: warmUpAmount{}, externalRewards: avl.NewTree(), } @@ -210,10 +210,10 @@ func calculateCollectReward(tokenId uint64, deposit Deposit) (*collectResult, er } result.externalRewards.Set(ictvId, externalRewardInfo{ - ictvId: ictvId, - tokenPath: external.tokenPath, + ictvId: ictvId, + tokenPath: external.tokenPath, fullAmount: fullAmount, - toGive: toGive, + toGive: toGive, }) } } @@ -225,13 +225,13 @@ func calculateCollectReward(tokenId uint64, deposit Deposit) (*collectResult, er // externalRewardResult contains all the data needed to emit the event type externalRewardResult struct { - ictvId string - tokenPath string - poolPath string + ictvId string + tokenPath string + poolPath string fullAmount uint64 - toGive uint64 - toUser uint64 - left uint64 + toGive uint64 + toUser uint64 + left uint64 } func applyExternalReward( @@ -258,8 +258,8 @@ func applyExternalReward( unwrap(toUser) } - positionsExternalWarmUpAmount[tokenId][reward.ictvId] = warmUpAmount{} - positionLastExternal[tokenId][reward.ictvId] = u256.Zero() + positionsExternalWarmUpAmount[tokenId][reward.ictvId] = warmUpAmount{} + positionLastExternal[tokenId][reward.ictvId] = u256.Zero() remaining := reward.fullAmount - reward.toGive transferByRegisterCall(reward.tokenPath, consts.PROTOCOL_FEE_ADDR, remaining) @@ -268,22 +268,22 @@ func applyExternalReward( incentives.Set(reward.ictvId, ictv) return externalRewardResult{ - ictvId: reward.ictvId, - tokenPath: reward.tokenPath, - poolPath: ictv.targetPoolPath, + ictvId: reward.ictvId, + tokenPath: reward.tokenPath, + poolPath: ictv.targetPoolPath, fullAmount: reward.fullAmount, - toGive: reward.toGive, - toUser: toUser, - left: remaining, + toGive: reward.toGive, + toUser: toUser, + left: remaining, } } // internalRewardResult contains all the data needed to emit the event type internalRewardResult struct { fullAmount uint64 - toGive uint64 - toUser uint64 - left uint64 + toGive uint64 + toUser uint64 + left uint64 } func applyInternalReward( @@ -310,9 +310,9 @@ func applyInternalReward( return internalRewardResult{ fullAmount: fullAmount, - toGive: toGive, - toUser: toUser, - left: left, + toGive: toGive, + toUser: toUser, + left: left, } } @@ -343,6 +343,9 @@ func calculateGnsBalance() uint64 { return gnsBalance(consts.STAKER_ADDR) - externalGnsAmount() - externalDepositGnsAmount() } +// CollectReward collects staked rewards for the given tokenId +// Returns poolPath +// ref: https://docs.gnoswap.io/contracts/staker/staker.gno#collectreward func CollectReward(tokenId uint64, unwrapResult bool) string { common.IsHalted() @@ -365,224 +368,121 @@ func CollectReward(tokenId uint64, unwrapResult bool) string { } external, internal := applyCollectReaward(result, unwrapResult) - + prevAddr, prevPkgPath := getPrev() for _, reward := range external { std.Emit( - "ProtocolFeeExternalPenalty", - "prevAddr", prevAddr, - "prevRealm", prevPkgPath, - "lpTokenId", ufmt.Sprintf("%d", tokenId), - "internal_poolPath", reward.poolPath, - "internal_incentiveId", reward.ictvId, - "internal_tokenPath", reward.tokenPath, - "internal_amount", ufmt.Sprintf("%d", reward.left), - ) - - std.Emit( - "CollectRewardExternal", - "prevAddr", prevAddr, - "prevRealm", prevPkgPath, - "lpTokenId", ufmt.Sprintf("%d", tokenId), - "internal_poolPath", reward.poolPath, - "internal_incentiveId", reward.ictvId, - "internal_rewardToken", reward.tokenPath, - "internal_recipient", result.owner.String(), - "internal_amount", ufmt.Sprintf("%d", reward.toUser), - "internal_unwrapResult", ufmt.Sprintf("%t", unwrapResult), - ) + "ProtocolFeeExternalPenalty", + "prevAddr", prevAddr, + "prevRealm", prevPkgPath, + "lpTokenId", ufmt.Sprintf("%d", tokenId), + "internal_poolPath", reward.poolPath, + "internal_incentiveId", reward.ictvId, + "internal_tokenPath", reward.tokenPath, + "internal_amount", ufmt.Sprintf("%d", reward.left), + ) + + std.Emit( + "CollectRewardExternal", + "prevAddr", prevAddr, + "prevRealm", prevPkgPath, + "lpTokenId", ufmt.Sprintf("%d", tokenId), + "internal_poolPath", reward.poolPath, + "internal_incentiveId", reward.ictvId, + "internal_rewardToken", reward.tokenPath, + "internal_recipient", result.owner.String(), + "internal_amount", ufmt.Sprintf("%d", reward.toUser), + "internal_unwrapResult", ufmt.Sprintf("%t", unwrapResult), + ) } if internal.toGive > 0 { - std.Emit( - "CommunityPoolEmissionPenalty", - "prevAddr", prevAddr, - "prevRealm", prevPkgPath, - "lpTokenId", ufmt.Sprintf("%d", tokenId), - "internal_poolPath", result.poolPath, - "internal_incentiveId", "INTERNAL", - "internal_tokenPath", consts.GNS_PATH, - "internal_amount", ufmt.Sprintf("%d", internal.left), - ) - - std.Emit( - "CollectRewardEmission", - "prevAddr", prevAddr, - "prevRealm", prevPkgPath, - "lpTokenId", ufmt.Sprintf("%d", tokenId), - "internal_poolPath", result.poolPath, - "internal_incentiveId", "INTERNAL", - "internal_rewardToken", consts.GNS_PATH, - "internal_recipient", result.owner.String(), - "internal_fullAmount", ufmt.Sprintf("%d", internal.fullAmount), - "internal_toGive", ufmt.Sprintf("%d", internal.toGive), - "internal_amount", ufmt.Sprintf("%d", internal.toUser), - "internal_unstakingFee", ufmt.Sprintf("%d", internal.toGive-internal.toUser), - "internal_left", ufmt.Sprintf("%d", internal.left), - ) - } + std.Emit( + "CommunityPoolEmissionPenalty", + "prevAddr", prevAddr, + "prevRealm", prevPkgPath, + "lpTokenId", ufmt.Sprintf("%d", tokenId), + "internal_poolPath", result.poolPath, + "internal_incentiveId", "INTERNAL", + "internal_tokenPath", consts.GNS_PATH, + "internal_amount", ufmt.Sprintf("%d", internal.left), + ) + + std.Emit( + "CollectRewardEmission", + "prevAddr", prevAddr, + "prevRealm", prevPkgPath, + "lpTokenId", ufmt.Sprintf("%d", tokenId), + "internal_poolPath", result.poolPath, + "internal_incentiveId", "INTERNAL", + "internal_rewardToken", consts.GNS_PATH, + "internal_recipient", result.owner.String(), + "internal_fullAmount", ufmt.Sprintf("%d", internal.fullAmount), + "internal_toGive", ufmt.Sprintf("%d", internal.toGive), + "internal_amount", ufmt.Sprintf("%d", internal.toUser), + "internal_unstakingFee", ufmt.Sprintf("%d", internal.toGive-internal.toUser), + "internal_left", ufmt.Sprintf("%d", internal.left), + ) + } return result.poolPath } -// CollectReward collects staked rewards for the given tokenId -// Returns poolPath -// ref: https://docs.gnoswap.io/contracts/staker/staker.gno#collectreward -// func CollectReward(tokenId uint64, unwrapResult bool) string { -// common.IsHalted() - -// en.MintAndDistributeGns() -// if consts.EMISSION_REFACTORED { -// CalcPoolPositionRefactor() -// } else { -// CalcPoolPosition() -// } - -// deposit, exist := deposits.Get(tokenId) -// if !exist { -// panic(addDetailToError( -// errDataNotFound, -// ufmt.Sprintf("staker.gno__CollectReward() || tokenId(%d) not staked", tokenId), -// )) -// } - -// caller := std.PrevRealm().Addr() -// if err := common.SatisfyCond(caller == deposit.owner); err != nil { -// panic(ufmt.Sprintf("%v: caller is not owner of tokenId(%d)", errNoPermission, tokenId)) -// } - -// poolPath := deposit.TargetPoolPath() - -// prevAddr, prevRealm := getPrev() - -// _, exist = positionExternal[tokenId] -// if exist { -// for _, external := range positionExternal[tokenId] { -// incentive, exists := incentives.Get(external.incentiveId) -// if !exists { -// continue -// } -// incentiveId := external.incentiveId - -// externalWarmUpAmount, exist := positionsExternalWarmUpAmount[tokenId][incentiveId] -// if !exist { -// continue -// } -// fullAmount := externalWarmUpAmount.full30 + externalWarmUpAmount.full50 + externalWarmUpAmount.full70 + externalWarmUpAmount.full100 -// toGive := externalWarmUpAmount.give30 + externalWarmUpAmount.give50 + externalWarmUpAmount.give70 + externalWarmUpAmount.full100 - -// if toGive == 0 { -// continue -// } - -// _this := positionExternal[tokenId][incentiveId] -// _this.tokenAmountX96 = u256.Zero() -// _this.tokenAmountFull += fullAmount -// _this.tokenAmountToGive += toGive -// positionExternal[tokenId][incentiveId] = _this - -// toUser := handleUnstakingFee(external.tokenPath, toGive, false, tokenId, incentive.targetPoolPath) - -// transferByRegisterCall(external.tokenPath, deposit.owner, toUser) -// if external.tokenPath == consts.WUGNOT_PATH && unwrapResult { -// unwrap(toUser) -// } - -// positionsExternalWarmUpAmount[tokenId][incentiveId] = warmUpAmount{} // JUST CLEAR -// positionLastExternal[tokenId][incentiveId] = u256.Zero() // JUST CLEAR - -// left := fullAmount - toGive -// transferByRegisterCall(external.tokenPath, consts.PROTOCOL_FEE_ADDR, left) - -// //////////////////////////////////////////////////////////// - -// std.Emit( -// "ProtocolFeeExternalPenalty", -// "prevAddr", prevAddr, -// "prevRealm", prevRealm, -// "lpTokenId", ufmt.Sprintf("%d", tokenId), -// "internal_poolPath", poolPath, -// "internal_incentiveId", incentiveId, -// "internal_tokenPath", external.tokenPath, -// "internal_amount", ufmt.Sprintf("%d", left), -// ) - -// incentive.rewardLeft = new(u256.Uint).Sub(incentive.rewardLeft, u256.NewUint(fullAmount)) -// incentives.Set(incentiveId, incentive) - -// if external.tokenPath == consts.GNS_PATH { -// externalGns[incentiveId] -= fullAmount -// } - -// std.Emit( -// "CollectRewardExternal", -// "prevAddr", prevAddr, -// "prevRealm", prevRealm, -// "lpTokenId", ufmt.Sprintf("%d", tokenId), -// "internal_poolPath", poolPath, -// "internal_incentiveId", incentiveId, -// "internal_rewardToken", external.tokenPath, -// "internal_recipient", deposit.owner.String(), -// "internal_amount", ufmt.Sprintf("%d", toUser), -// "internal_unwrapResult", ufmt.Sprintf("%t", unwrapResult), -// ) -// } -// } - -// // INTERNAL gns emission -// internalWarmUpAmount, exist := positionsInternalWarmUpAmount[tokenId] -// if !exist { -// return poolPath -// } -// fullAmount := internalWarmUpAmount.full30 + internalWarmUpAmount.full50 + internalWarmUpAmount.full70 + internalWarmUpAmount.full100 -// toGive := internalWarmUpAmount.give30 + internalWarmUpAmount.give50 + internalWarmUpAmount.give70 + internalWarmUpAmount.full100 - -// if toGive == 0 { -// return poolPath -// } -// toUser := handleUnstakingFee(consts.GNS_PATH, toGive, true, tokenId, poolPath) -// gns.Transfer(a2u(deposit.owner), toUser) - -// // delete(positionsInternalWarmUpAmount, tokenId) // DO NOT DELETE -// positionsInternalWarmUpAmount[tokenId] = warmUpAmount{} // JUST CLEAR - -// poolGns[poolPath] -= fullAmount - -// left := fullAmount - toGive -// gns.Transfer(a2u(consts.COMMUNITY_POOL_ADDR), left) -// std.Emit( -// "CommunityPoolEmissionPenalty", -// "prevAddr", prevAddr, -// "prevRealm", prevRealm, -// "lpTokenId", ufmt.Sprintf("%d", tokenId), -// "internal_poolPath", poolPath, -// "internal_incentiveId", "INTERNAL", -// "internal_tokenPath", consts.GNS_PATH, -// "internal_amount", ufmt.Sprintf("%d", left), -// ) - -// std.Emit( -// "CollectRewardEmission", -// "prevAddr", prevAddr, -// "prevRealm", prevRealm, -// "lpTokenId", ufmt.Sprintf("%d", tokenId), -// "internal_poolPath", poolPath, -// "internal_incentiveId", "INTERNAL", -// "internal_rewardToken", consts.GNS_PATH, -// "internal_recipient", deposit.owner.String(), -// "internal_fullAmount", ufmt.Sprintf("%d", fullAmount), -// "internal_toGive", ufmt.Sprintf("%d", toGive), -// "internal_amount", ufmt.Sprintf("%d", toUser), -// "internal_unstakingFee", ufmt.Sprintf("%d", toGive-toUser), -// "internal_left", ufmt.Sprintf("%d", left), -// ) - -// // UPDATE stakerGns Balance for calculate_pool_position_reward -// lastCalculatedBalance = gnsBalance(consts.STAKER_ADDR) - externalGnsAmount() - externalDepositGnsAmount() - -// return poolPath -// } +type unstakeInput struct { + tokenId uint64 + unwrap bool + deposit Deposit +} + +func newUnstakeInput(tokenId uint64, unwrap bool, deposit Deposit) unstakeInput { + return unstakeInput{ + tokenId: tokenId, + unwrap: unwrap, + deposit: deposit, + } +} + +type unstakeOutput struct { + tokenId uint64 + owner std.Address + poolPath string + token0Amount string + token1Amount string + from std.Address + to std.Address +} + +func newUnstakeOutput(poolPath string, token0Amount, token1Amount string, input unstakeInput) *unstakeOutput { + return &unstakeOutput{ + tokenId: input.tokenId, + owner: input.deposit.owner, + poolPath: poolPath, + token0Amount: token0Amount, + token1Amount: token1Amount, + from: consts.STAKER_ADDR, + to: input.deposit.owner, + } +} + +// TODO: change type of positionInternalWarmUpAmount if needed. +func cleanupStakeData( + tokenId uint64, + positionGns map[uint64]uint64, + positionsInternalWarmUpAmount map[uint64]warmUpAmount, +) { + delete(positionGns, tokenId) + delete(positionsInternalWarmUpAmount, tokenId) + deposits.Remove(tokenId) +} + +func applyUnstake(manager *RewardManager, input unstakeInput) *RewardManager { + internalEmission := manager.GetInternalEmissionReward() + internalEmission.RemoveInRangePosition(input.deposit.targetPoolPath, input.tokenId) + manager.SetInternalEmissionReward(internalEmission) + + return manager +} // UnstakeToken unstakes the LP token from the staker and collects all reward from tokenId // ref: https://docs.gnoswap.io/contracts/staker/staker.gno#unstaketoken @@ -599,31 +499,28 @@ func UnstakeToken(tokenId uint64, unwrapResult bool) (string, string, string) { // unstaked status deposit, exist := deposits.Get(tokenId) if !exist { - panic(addDetailToError( - errDataNotFound, - ufmt.Sprintf("staker.gno__UnstakeToken() || tokenId(%d) not staked", tokenId), - )) + msg := ufmt.Sprintf("%v: tokenId(%d) not staked", errDataNotFound, tokenId) + panic(msg) } // Claim All Rewards CollectReward(tokenId, unwrapResult) - delete(positionGns, tokenId) - delete(positionsInternalWarmUpAmount, tokenId) - deposits.Remove(tokenId) + poolPath := pn.PositionGetPositionPoolKey(tokenId) + token0Amount, token1Amount := getTokenPairBalanceFromPosition(tokenId) + + input := newUnstakeInput(tokenId, unwrapResult, deposit) + output := newUnstakeOutput(poolPath, token0Amount, token1Amount, input) + + cleanupStakeData(input.tokenId, positionGns, positionsInternalWarmUpAmount) - rewardManger := getRewardManager() - internalEmission := rewardManger.GetInternalEmissionReward() - internalEmission.RemoveInRangePosition(deposit.targetPoolPath, tokenId) - rewardManger.SetInternalEmissionReward(internalEmission) + manager := getRewardManager() + applyUnstake(manager, input) // transfer NFT ownership to origin owner gnft.TransferFrom(a2u(consts.STAKER_ADDR), a2u(deposit.owner), tid(tokenId)) pn.SetPositionOperator(tokenId, consts.ZERO_ADDRESS) - poolPath := pn.PositionGetPositionPoolKey(tokenId) - token0Amount, token1Amount := getTokenPairBalanceFromPosition(tokenId) - prevAddr, prevRealm := getPrev() std.Emit( "UnstakeToken", @@ -631,14 +528,14 @@ func UnstakeToken(tokenId uint64, unwrapResult bool) (string, string, string) { "prevRealm", prevRealm, "lpTokenId", ufmt.Sprintf("%d", tokenId), "unwrapResult", ufmt.Sprintf("%t", unwrapResult), - "internal_poolPath", poolPath, - "internal_from", GetOrigPkgAddr().String(), - "internal_to", deposit.owner.String(), - "internal_amount0", token0Amount, - "internal_amount1", token1Amount, + "internal_poolPath", output.poolPath, + "internal_from", output.from.String(), + "internal_to", output.to.String(), + "internal_amount0", output.token0Amount, + "internal_amount1", output.token1Amount, ) - return poolPath, token0Amount, token1Amount + return output.poolPath, output.token0Amount, output.token1Amount } // requireTokenOwnership checks if the caller has the token ownership diff --git a/staker/staker_test.gno b/staker/staker_test.gno index c36e4048..fc636e17 100644 --- a/staker/staker_test.gno +++ b/staker/staker_test.gno @@ -5,121 +5,491 @@ import ( "testing" "time" - "gno.land/p/demo/avl" + "gno.land/r/demo/users" "gno.land/p/demo/testutils" "gno.land/p/demo/uassert" + + u256 "gno.land/p/gnoswap/uint256" + "gno.land/r/gnoswap/v1/consts" ) func TestCalculateCollectReward(t *testing.T) { - tokenId := uint64(1) - owner := testutils.TestAddress("owner") - poolPath := "token0:token1:3000" - - tests := []struct { - name string - setup func() - want struct { - tokenId uint64 - owner std.Address - poolPath string - hasError bool - } - }{ - { + tokenId := uint64(1) + owner := testutils.TestAddress("owner") + poolPath := "token0:token1:3000" + + tests := []struct { + name string + setup func() + want struct { + tokenId uint64 + owner std.Address + poolPath string + hasError bool + } + }{ + { name: "normal case - no reward", - setup: func() { - deposits.Set(tokenId, newDeposit( - owner, - 1, - time.Now().Unix(), - 100, - poolPath, - )) - }, - want: struct { - tokenId uint64 - owner std.Address - poolPath string - hasError bool - }{ - tokenId: tokenId, - owner: owner, - poolPath: poolPath, - hasError: false, - }, - }, - { + setup: func() { + deposits.Set(tokenId, newDeposit( + owner, + 1, + time.Now().Unix(), + 100, + poolPath, + )) + }, + want: struct { + tokenId uint64 + owner std.Address + poolPath string + hasError bool + }{ + tokenId: tokenId, + owner: owner, + poolPath: poolPath, + hasError: false, + }, + }, + { name: "external reward", - setup: func() { - deposits.Set(tokenId, newDeposit( - owner, - 1, - time.Now().Unix(), - 100, - poolPath, - )) - - ictvId := "incentive1" - incentives.Set(ictvId, ExternalIncentive{ - targetPoolPath: poolPath, - rewardToken: "rewardToken", - }) + setup: func() { + deposits.Set(tokenId, newDeposit( + owner, + 1, + time.Now().Unix(), + 100, + poolPath, + )) + + ictvId := "incentive1" + incentives.Set(ictvId, ExternalIncentive{ + targetPoolPath: poolPath, + rewardToken: "rewardToken", + }) // TODO: update type if needed (avl.Tree ?) - positionExternal[tokenId] = map[string]externalRewards{ + positionExternal[tokenId] = map[string]externalRewards{ ictvId: { incentiveId: ictvId, - poolPath: poolPath, - tokenPath: "rewardToken", + poolPath: poolPath, + tokenPath: "rewardToken", }, } // TODO: update type if needed (avl.Tree ?) - positionsExternalWarmUpAmount[tokenId] = map[string]warmUpAmount{ + positionsExternalWarmUpAmount[tokenId] = map[string]warmUpAmount{ ictvId: { - full30: 30, - give30: 15, - full50: 50, - give50: 25, - full70: 70, - give70: 35, + full30: 30, + give30: 15, + full50: 50, + give50: 25, + full70: 70, + give70: 35, full100: 100, }, } - }, - want: struct { - tokenId uint64 - owner std.Address - poolPath string - hasError bool - }{ - tokenId: tokenId, - owner: owner, - poolPath: poolPath, - hasError: false, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.setup() - - deposit := deposits.MustGet(tokenId) - result, err := calculateCollectReward(tokenId, deposit) - - if tt.want.hasError { - if err == nil { - t.Error("expected error but not occurred") - } - return - } + }, + want: struct { + tokenId uint64 + owner std.Address + poolPath string + hasError bool + }{ + tokenId: tokenId, + owner: owner, + poolPath: poolPath, + hasError: false, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setup() + + deposit := deposits.MustGet(tokenId) + result, err := calculateCollectReward(tokenId, deposit) + + if tt.want.hasError { + if err == nil { + t.Error("expected error but not occurred") + } + return + } uassert.NoError(t, err) uassert.Equal(t, result.tokenId, tt.want.tokenId) uassert.Equal(t, result.owner, tt.want.owner) uassert.Equal(t, result.poolPath, tt.want.poolPath) - }) - } + }) + } +} + +func TestApplyExternalReward(t *testing.T) { + tokenId := uint64(1) + owner := testutils.TestAddress("owner") + ictvId := "incentive1" + tokenPath := barPath + poolPath := "gno.land/r/onbloc/bar:gno.land/r/onbloc/foo:3000" + + mockToken := struct { + GRC20Interface + }{ + GRC20Interface: BarToken{}, + } + + tests := []struct { + name string + setup func() + reward externalRewardInfo + want struct { + toUser uint64 + remaining uint64 + tokenAmountX96 *u256.Uint + tokenAmountFull uint64 + tokenAmountToGive uint64 + } + }{ + { + name: "no incentive", + setup: func() { + incentives = newIncentives() + }, + reward: externalRewardInfo{ + ictvId: "non_existing_incentive", + tokenPath: tokenPath, + fullAmount: 100, + toGive: 60, + }, + want: struct { + toUser uint64 + remaining uint64 + tokenAmountX96 *u256.Uint + tokenAmountFull uint64 + tokenAmountToGive uint64 + }{ + toUser: 0, + remaining: 0, + tokenAmountX96: nil, + tokenAmountFull: 0, + tokenAmountToGive: 0, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + std.TestSetOrigCaller(users.Resolve(admin)) + tt.setup() + + result := applyExternalReward(tokenId, tt.reward, owner, false) + uassert.Equal(t, result.toUser, tt.want.toUser) + uassert.Equal(t, result.left, tt.want.remaining) + + external := positionExternal[tokenId][tt.reward.ictvId] + uassert.Equal(t, external.tokenAmountX96.ToString(), tt.want.tokenAmountX96.ToString()) + uassert.Equal(t, external.tokenAmountFull, tt.want.tokenAmountFull) + + uassert.Equal(t, external.tokenAmountToGive, tt.want.tokenAmountToGive) + + warmUp := positionsExternalWarmUpAmount[tokenId][tt.reward.ictvId] + uassert.Equal(t, warmUp.totalFull(), uint64(0)) + uassert.Equal(t, warmUp.totalGive(), uint64(0)) + }) + } +} + +func TestNewUnstakeInput(t *testing.T) { + tests := []struct { + name string + tokenId uint64 + unwrap bool + deposit Deposit + want unstakeInput + }{ + { + name: "should create unstake input with false unwrap", + tokenId: 1, + unwrap: false, + deposit: Deposit{ + owner: std.Address("test1"), + targetPoolPath: "pool1", + }, + want: unstakeInput{ + tokenId: 1, + unwrap: false, + deposit: Deposit{ + owner: std.Address("test1"), + targetPoolPath: "pool1", + }, + }, + }, + { + name: "should create unstake input with true unwrap", + tokenId: 2, + unwrap: true, + deposit: Deposit{ + owner: std.Address("test2"), + targetPoolPath: "pool2", + }, + want: unstakeInput{ + tokenId: 2, + unwrap: true, + deposit: Deposit{ + owner: std.Address("test2"), + targetPoolPath: "pool2", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := newUnstakeInput(tt.tokenId, tt.unwrap, tt.deposit) + uassert.Equal(t, got.tokenId, tt.want.tokenId) + uassert.Equal(t, got.unwrap, tt.want.unwrap) + + assertDeposit(t, got.deposit, tt.want.deposit) + }) + } +} + +func TestNewUnstakeOutput(t *testing.T) { + test1 := testutils.TestAddress("test1") + test2 := testutils.TestAddress("test2") + test3 := testutils.TestAddress("test3") + + tests := []struct { + name string + poolPath string + token0Amount string + token1Amount string + input unstakeInput + want *unstakeOutput + }{ + { + name: "should create unstake output with zero amounts", + poolPath: "pool1", + token0Amount: "0", + token1Amount: "0", + input: unstakeInput{ + tokenId: 1, + unwrap: false, + deposit: Deposit{ + owner: test1, + targetPoolPath: "pool1", + }, + }, + want: &unstakeOutput{ + tokenId: 1, + owner: test1, + poolPath: "pool1", + token0Amount: "0", + token1Amount: "0", + from: consts.STAKER_ADDR, + to: test1, + }, + }, + { + name: "should create unstake output with non-zero amounts", + poolPath: "pool2", + token0Amount: "100", + token1Amount: "200", + input: unstakeInput{ + tokenId: 2, + unwrap: true, + deposit: Deposit{ + owner: test2, + targetPoolPath: "pool2", + }, + }, + want: &unstakeOutput{ + tokenId: 2, + owner: test2, + poolPath: "pool2", + token0Amount: "100", + token1Amount: "200", + from: consts.STAKER_ADDR, + to: test2, + }, + }, + { + name: "should create unstake output with different pool path", + poolPath: "newPool", + token0Amount: "150", + token1Amount: "300", + input: unstakeInput{ + tokenId: 3, + unwrap: false, + deposit: Deposit{ + owner: test3, + targetPoolPath: "oldPool", + }, + }, + want: &unstakeOutput{ + tokenId: 3, + owner: test3, + poolPath: "newPool", + token0Amount: "150", + token1Amount: "300", + from: consts.STAKER_ADDR, + to: test3, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := newUnstakeOutput(tt.poolPath, tt.token0Amount, tt.token1Amount, tt.input) + assertUnstakeOutput(t, got, tt.want) + }) + } +} + +func TestApplyUnstake(t *testing.T) { + var notExistTokenId uint64 = 999 + + tests := []struct { + name string + setup func() (*RewardManager, unstakeInput) + verify func(*RewardManager) bool + }{ + { + name: "Normal unstaking test", + setup: func() (*RewardManager, unstakeInput) { + rm := NewRewardManager() + internalReward := NewInternalEmissionReward() + recipientsMap := NewRewardRecipientMap() + poolLiquidity := NewPoolLiquidity() + + inRangeLiquidity := NewInRangeLiquidity() + inRangeLiquidity.SetLiquidity(u256.NewUint(1000)) + inRangeLiquidity.SetLiquidityRatio(u256.NewUint(100)) + inRangeLiquidity.SetStakedHeight(100) + + poolLiquidity.AddInRangePosition(1, inRangeLiquidity) + recipientsMap.SetPoolLiquidity("test/pool", poolLiquidity) + internalReward.SetRewardRecipientsMap(recipientsMap) + rm.SetInternalEmissionReward(internalReward) + + input := unstakeInput{ + tokenId: 1, + deposit: Deposit{ + targetPoolPath: "test/pool", + stakeHeight: 100, + }, + } + + return rm, input + }, + verify: func(rm *RewardManager) bool { + internalReward := rm.GetInternalEmissionReward() + if internalReward == nil { + return false + } + + recipientsMap := internalReward.GetRewardRecipientsMap() + if recipientsMap == nil { + return false + } + + poolLiquidity := recipientsMap.GetPoolLiquidity("test/pool") + if poolLiquidity == nil { + return false + } + + position := poolLiquidity.GetInRangeLiquidity(1) + if position == nil { + return false + } + + if !position.GetLiquidity().IsZero() { + return false + } + if !position.GetLiquidityRatio().IsZero() { + return false + } + if position.GetStakedHeight() != 0 { + return false + } + + return true + }, + }, + { + name: "Non-existent pool unstaking test", + setup: func() (*RewardManager, unstakeInput) { + rm := NewRewardManager() + input := unstakeInput{ + tokenId: 1, + deposit: Deposit{ + targetPoolPath: "non/existent/pool", + stakeHeight: 100, + }, + } + return rm, input + }, + verify: func(rm *RewardManager) bool { + return rm.GetInternalEmissionReward() != nil + }, + }, + { + name: "Non-existent token ID unstaking test", + setup: func() (*RewardManager, unstakeInput) { + rm := NewRewardManager() + internalReward := NewInternalEmissionReward() + recipientsMap := NewRewardRecipientMap() + poolLiquidity := NewPoolLiquidity() + recipientsMap.SetPoolLiquidity("test/pool", poolLiquidity) + internalReward.SetRewardRecipientsMap(recipientsMap) + rm.SetInternalEmissionReward(internalReward) + + input := unstakeInput{ + tokenId: notExistTokenId, + deposit: Deposit{ + targetPoolPath: "test/pool", + stakeHeight: 100, + }, + } + return rm, input + }, + verify: func(rm *RewardManager) bool { + return rm.GetInternalEmissionReward() != nil + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rm, input := tt.setup() + + resultRM := applyUnstake(rm, input) + + if !tt.verify(resultRM) { + t.Errorf("%s: verification failed", tt.name) + } + }) + } +} + +func assertDeposit(t *testing.T, got, want Deposit) { + t.Helper() + uassert.Equal(t, got.owner, want.owner) + uassert.Equal(t, got.targetPoolPath, want.targetPoolPath) + uassert.Equal(t, got.numberOfStakes, want.numberOfStakes) + uassert.Equal(t, got.stakeTimestamp, want.stakeTimestamp) + uassert.Equal(t, got.stakeHeight, want.stakeHeight) +} + +func assertUnstakeOutput(t *testing.T, got, want *unstakeOutput) { + t.Helper() + uassert.Equal(t, got.tokenId, want.tokenId) + uassert.Equal(t, got.owner, want.owner) + uassert.Equal(t, got.poolPath, want.poolPath) + uassert.Equal(t, got.token0Amount, want.token0Amount) + uassert.Equal(t, got.token1Amount, want.token1Amount) + uassert.Equal(t, got.from, want.from) + uassert.Equal(t, got.to, want.to) } diff --git a/staker/staker_type.gno b/staker/staker_type.gno index 97ab18b0..ef4b7e72 100644 --- a/staker/staker_type.gno +++ b/staker/staker_type.gno @@ -1,12 +1,16 @@ package staker +import ( + "gno.land/p/demo/ufmt" +) + var ( /* internal */ poolTiers = newPoolTiers() /* external */ poolIncentives = newPoolIncentives() - incentives = newIncentives() + incentives = newIncentives() /* common */ deposits = newDeposits() @@ -85,6 +89,14 @@ func (d Deposits) Get(tokenId uint64) (Deposit, bool) { return deposit, exist } +func (d Deposits) MustGet(tokenId uint64) Deposit { + deposit, exist := d.Get(tokenId) + if !exist { + panic(ufmt.Sprintf("deposit not found for tokenId: %d", tokenId)) + } + return deposit +} + func (d Deposits) Set(tokenId uint64, deposit Deposit) { d[tokenId] = deposit }