calculate_pool_position_reward.gno
6.99 Kb ยท 215 lines
1package v1
2
3import (
4 "gno.land/p/nt/avl"
5 sr "gno.land/r/gnoswap/staker"
6)
7
8// Reward is a struct for storing reward for a position.
9// Internal reward is the GNS reward, external reward is the reward for other incentives.
10// Penalties are the amount that is deducted from the reward due to the position's warmup.
11type Reward struct {
12 Internal int64
13 InternalPenalty int64
14 External map[string]int64 // Incentive ID -> TokenAmount
15 ExternalPenalty map[string]int64 // Incentive ID -> TokenAmount
16}
17
18// calculate total position rewards and penalties
19func (s *stakerV1) calcPositionReward(currentHeight, currentTimestamp int64, positionId uint64) Reward {
20 rewards := s.calculatePositionReward(&CalcPositionRewardParam{
21 CurrentHeight: currentHeight,
22 CurrentTime: currentTimestamp,
23 Deposits: s.getDeposits(),
24 Pools: s.getPools(),
25 PoolTier: s.getPoolTier(),
26 PositionId: positionId,
27 })
28
29 internal := int64(0)
30 internalPenalty := int64(0)
31
32 rewardLen := len(rewards)
33 externalReward := make(map[string]int64, rewardLen)
34 externalPenalty := make(map[string]int64, rewardLen)
35
36 for _, reward := range rewards {
37 internal = safeAddInt64(internal, reward.Internal)
38 internalPenalty = safeAddInt64(internalPenalty, reward.InternalPenalty)
39
40 for incentive, amount := range reward.External {
41 externalReward[incentive] = safeAddInt64(externalReward[incentive], amount)
42 }
43
44 for incentive, penalty := range reward.ExternalPenalty {
45 externalPenalty[incentive] = safeAddInt64(externalPenalty[incentive], penalty)
46 }
47 }
48
49 return Reward{
50 Internal: internal,
51 InternalPenalty: internalPenalty,
52 External: externalReward,
53 ExternalPenalty: externalPenalty,
54 }
55}
56
57// CalcPositionRewardParam is a struct for calculating position reward
58type CalcPositionRewardParam struct {
59 // Environmental variables
60 CurrentHeight int64
61 CurrentTime int64
62 Deposits *Deposits
63 Pools *Pools
64 PoolTier *PoolTier
65
66 // Position variables
67 PositionId uint64
68}
69
70func (s *stakerV1) calculatePositionReward(param *CalcPositionRewardParam) []Reward {
71 // cache per-pool rewards in the internal incentive(tiers)
72 param.PoolTier.cacheReward(param.CurrentHeight, param.CurrentTime, param.Pools)
73 s.updatePoolTier(param.PoolTier)
74
75 deposit := param.Deposits.get(param.PositionId)
76 depositResolver := NewDepositResolver(deposit)
77 poolPath := deposit.TargetPoolPath()
78
79 pool, ok := param.Pools.Get(poolPath)
80 if !ok {
81 pool = sr.NewPool(poolPath, param.CurrentTime)
82 param.Pools.set(poolPath, pool)
83 }
84 poolResolver := NewPoolResolver(pool)
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 return poolResolver.processUnclaimableReward(endTimestamp)
168}
169
170// update deposit's incentive list with new incentives created since last update
171func (s *stakerV1) getExternalIncentiveIdsBy(poolPath string, startTime, endTime int64) []string {
172 currentIncentiveIds := make([]string, 0)
173
174 incentivesByTime := s.getExternalIncentivesByCreationTime()
175
176 incentivesByTime.Iterate(startTime, endTime, func(_ int64, value any) bool {
177 // Value is a slice of incentive IDs (handles timestamp collisions)
178 poolIncentiveIds, ok := value.(*avl.Tree)
179 if !ok {
180 return false
181 }
182
183 incentiveIdsValue, exists := poolIncentiveIds.Get(poolPath)
184 if !exists {
185 return false
186 }
187
188 incentiveIds, ok := incentiveIdsValue.([]string)
189 if !ok {
190 return false
191 }
192
193 currentIncentiveIds = append(currentIncentiveIds, incentiveIds...)
194
195 return false
196 })
197
198 return currentIncentiveIds
199}
200
201// getInitialCollectTime determines the initial collection time for an incentive
202// by taking the maximum of the deposit's stake time and the incentive's start time.
203// This ensures rewards are only calculated from when both conditions are met:
204// - The position must be staked (deposit.stakeTime)
205// - The incentive must be active (incentive.startTimestamp)
206//
207// This function is used for lazy initialization when a position collects
208// from an incentive for the first time, avoiding the need to iterate through
209// all deposits when a new incentive is created.
210func getInitialCollectTime(deposit *sr.Deposit, incentive *sr.ExternalIncentive) int64 {
211 if deposit.StakeTime() > incentive.StartTimestamp() {
212 return deposit.StakeTime()
213 }
214 return incentive.StartTimestamp()
215}