calculate_pool_position_reward.gno
7.07 Kb · 216 lines
1package staker
2
3import (
4 "gno.land/p/gnoswap/gnsmath"
5 bptree "gno.land/p/nt/bptree/v0"
6 sr "gno.land/r/gnoswap/staker"
7)
8
9// Reward is a struct for storing reward for a position.
10// Internal reward is the GNS reward, external reward is the reward for other incentives.
11// Penalties are the amount that is deducted from the reward due to the position's warmup.
12type Reward struct {
13 Internal int64
14 InternalPenalty int64
15 External map[string]int64 // Incentive ID -> TokenAmount
16 ExternalPenalty map[string]int64 // Incentive ID -> TokenAmount
17}
18
19// calculate total position rewards and penalties
20func (s *stakerV1) calcPositionReward(currentHeight, currentTimestamp int64, positionId uint64) Reward {
21 rewards := s.calculatePositionReward(&CalcPositionRewardParam{
22 CurrentHeight: currentHeight,
23 CurrentTime: currentTimestamp,
24 Deposits: s.getDeposits(),
25 Pools: s.getPools(),
26 PoolTier: s.getPoolTier(),
27 PositionId: positionId,
28 })
29
30 internal := int64(0)
31 internalPenalty := int64(0)
32
33 rewardLen := len(rewards)
34 externalReward := make(map[string]int64, rewardLen)
35 externalPenalty := make(map[string]int64, rewardLen)
36
37 for _, reward := range rewards {
38 internal = gnsmath.SafeAddInt64(internal, reward.Internal)
39 internalPenalty = gnsmath.SafeAddInt64(internalPenalty, reward.InternalPenalty)
40
41 for incentive, amount := range reward.External {
42 externalReward[incentive] = gnsmath.SafeAddInt64(externalReward[incentive], amount)
43 }
44
45 for incentive, penalty := range reward.ExternalPenalty {
46 externalPenalty[incentive] = gnsmath.SafeAddInt64(externalPenalty[incentive], penalty)
47 }
48 }
49
50 return Reward{
51 Internal: internal,
52 InternalPenalty: internalPenalty,
53 External: externalReward,
54 ExternalPenalty: externalPenalty,
55 }
56}
57
58// CalcPositionRewardParam is a struct for calculating position reward
59type CalcPositionRewardParam struct {
60 // Environmental variables
61 CurrentHeight int64
62 CurrentTime int64
63 Deposits *Deposits
64 Pools *Pools
65 PoolTier *PoolTier
66
67 // Position variables
68 PositionId uint64
69}
70
71func (s *stakerV1) calculatePositionReward(param *CalcPositionRewardParam) []Reward {
72 deposit := param.Deposits.get(param.PositionId)
73 depositResolver := NewDepositResolver(deposit)
74 poolPath := deposit.TargetPoolPath()
75
76 pool, ok := param.Pools.Get(poolPath)
77 if !ok {
78 pool = sr.NewPool(poolPath, param.CurrentTime)
79 param.Pools.set(poolPath, pool)
80 }
81 poolResolver := NewPoolResolver(pool)
82
83 // Cache reward/accumulators only for the pool we are currently calculating.
84 param.PoolTier.cacheRewardForPool(param.CurrentTime, param.Pools, poolPath)
85
86 lastCollectTime := depositResolver.InternalRewardLastCollectTime()
87
88 // Initializes reward/penalty arrays for rewards and penalties for each warmup
89 rewardState := poolResolver.RewardStateOf(deposit)
90
91 // Calculate internal rewards regardless of current tier status
92 // The reward cache system will automatically handle periods with 0 rewards
93 // This allows collecting rewards earned while the pool was in a tier,
94 // while preventing new rewards after tier removal
95 calculatedInternalRewards, calculatedInternalPenalties := rewardState.calculateInternalReward(lastCollectTime, param.CurrentTime)
96
97 warmupLen := len(deposit.Warmups())
98 rewards := make([]Reward, warmupLen)
99 for i := 0; i < warmupLen; i++ {
100 rewards[i] = Reward{
101 Internal: calculatedInternalRewards[i],
102 InternalPenalty: calculatedInternalPenalties[i],
103 External: make(map[string]int64),
104 ExternalPenalty: make(map[string]int64),
105 }
106 }
107 rewardState.reset()
108
109 lastExternalIncentiveUpdatedAt := depositResolver.LastExternalIncentiveUpdatedAt()
110
111 // update deposit's incentive list with new incentives created since last update
112 if lastExternalIncentiveUpdatedAt < param.CurrentTime {
113 // get new incentives created since last update
114 currentIncentiveIds := s.getExternalIncentiveIdsBy(poolPath, lastExternalIncentiveUpdatedAt, param.CurrentTime)
115
116 // add new created incentives to deposit
117 for _, incentiveId := range currentIncentiveIds {
118 deposit.AddExternalIncentiveId(incentiveId)
119 }
120
121 deposit.SetLastExternalIncentiveUpdatedAt(param.CurrentTime)
122 }
123
124 incentivesResolver := poolResolver.IncentivesResolver()
125
126 // Use deposit's indexed incentive IDs instead of iterating all pool incentives
127 deposit.IterateExternalIncentiveIds(func(incentiveId string) bool {
128 incentive, ok := incentivesResolver.Get(incentiveId)
129 if !ok {
130 return false
131 }
132
133 incentiveResolver := NewExternalIncentiveResolver(incentive)
134
135 // Check if incentive is active during this specific collection period
136 if !incentiveResolver.IsStarted(param.CurrentTime) {
137 return false
138 }
139
140 // External incentivized pool.
141 // Calculate reward for each warmup using per-incentive lastCollectTime
142 externalLastCollectTime := depositResolver.ExternalRewardLastCollectTime(incentiveId)
143 externalReward, externalPenalty := rewardState.calculateExternalReward(externalLastCollectTime, param.CurrentTime, incentive)
144
145 for i := range externalReward {
146 if externalReward[i] > 0 || externalPenalty[i] > 0 {
147 rewards[i].External[incentiveId] = externalReward[i]
148 rewards[i].ExternalPenalty[incentiveId] = externalPenalty[i]
149 }
150 }
151
152 rewardState.reset()
153
154 return false
155 })
156
157 return rewards
158}
159
160// calculates internal unclaimable reward for the pool
161func (s *stakerV1) processUnClaimableReward(poolPath string, endTimestamp int64) int64 {
162 pool, ok := s.getPools().Get(poolPath)
163 if !ok {
164 return 0
165 }
166 poolResolver := NewPoolResolver(pool)
167
168 return poolResolver.processUnclaimableReward(endTimestamp)
169}
170
171// update deposit's incentive list with new incentives created since last update
172func (s *stakerV1) getExternalIncentiveIdsBy(poolPath string, startTime, endTime int64) []string {
173 currentIncentiveIds := make([]string, 0)
174
175 incentivesByTime := s.getExternalIncentivesByCreationTime()
176
177 incentivesByTime.Iterate(startTime, endTime, func(_ int64, value any) bool {
178 // Value is a slice of incentive IDs (handles timestamp collisions)
179 poolIncentiveIds, ok := value.(*bptree.BPTree)
180 if !ok {
181 return false
182 }
183
184 incentiveIdsValue := poolIncentiveIds.Get(poolPath)
185 if incentiveIdsValue == nil {
186 return false
187 }
188
189 incentiveIds, ok := incentiveIdsValue.([]string)
190 if !ok {
191 return false
192 }
193
194 currentIncentiveIds = append(currentIncentiveIds, incentiveIds...)
195
196 return false
197 })
198
199 return currentIncentiveIds
200}
201
202// getInitialCollectTime determines the initial collection time for an incentive
203// by taking the maximum of the deposit's stake time and the incentive's start time.
204// This ensures rewards are only calculated from when both conditions are met:
205// - The position must be staked (deposit.stakeTime)
206// - The incentive must be active (incentive.startTimestamp)
207//
208// This function is used for lazy initialization when a position collects
209// from an incentive for the first time, avoiding the need to iterate through
210// all deposits when a new incentive is created.
211func getInitialCollectTime(deposit *sr.Deposit, incentive *sr.ExternalIncentive) int64 {
212 if deposit.StakeTime() > incentive.StartTimestamp() {
213 return deposit.StakeTime()
214 }
215 return incentive.StartTimestamp()
216}