calculate_pool_position_reward.gno
6.99 Kb · 214 lines
1package v1
2
3import (
4 bptree "gno.land/p/nt/bptree/v0"
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 deposit := param.Deposits.get(param.PositionId)
72 depositResolver := NewDepositResolver(deposit)
73 poolPath := deposit.TargetPoolPath()
74
75 pool, ok := param.Pools.Get(poolPath)
76 if !ok {
77 pool = sr.NewPool(poolPath, param.CurrentTime)
78 param.Pools.set(poolPath, pool)
79 }
80 poolResolver := NewPoolResolver(pool)
81
82 // Cache reward/accumulators only for the pool we are currently calculating.
83 param.PoolTier.cacheRewardForPool(param.CurrentTime, param.Pools, poolPath)
84
85 lastCollectTime := depositResolver.InternalRewardLastCollectTime()
86
87 // Initializes reward/penalty arrays for rewards and penalties for each warmup
88 rewardState := poolResolver.RewardStateOf(deposit)
89
90 // Calculate internal rewards regardless of current tier status
91 // The reward cache system will automatically handle periods with 0 rewards
92 // This allows collecting rewards earned while the pool was in a tier,
93 // while preventing new rewards after tier removal
94 calculatedInternalRewards, calculatedInternalPenalties := rewardState.calculateInternalReward(lastCollectTime, param.CurrentTime)
95
96 warmupLen := len(deposit.Warmups())
97 rewards := make([]Reward, warmupLen)
98 for i := 0; i < warmupLen; i++ {
99 rewards[i] = Reward{
100 Internal: calculatedInternalRewards[i],
101 InternalPenalty: calculatedInternalPenalties[i],
102 External: make(map[string]int64),
103 ExternalPenalty: make(map[string]int64),
104 }
105 }
106 rewardState.reset()
107
108 lastExternalIncentiveUpdatedAt := depositResolver.LastExternalIncentiveUpdatedAt()
109
110 // update deposit's incentive list with new incentives created since last update
111 if lastExternalIncentiveUpdatedAt < param.CurrentTime {
112 // get new incentives created since last update
113 currentIncentiveIds := s.getExternalIncentiveIdsBy(poolPath, lastExternalIncentiveUpdatedAt, param.CurrentTime)
114
115 // add new created incentives to deposit
116 for _, incentiveId := range currentIncentiveIds {
117 deposit.AddExternalIncentiveId(incentiveId)
118 }
119
120 deposit.SetLastExternalIncentiveUpdatedAt(param.CurrentTime)
121 }
122
123 incentivesResolver := poolResolver.IncentivesResolver()
124
125 // Use deposit's indexed incentive IDs instead of iterating all pool incentives
126 deposit.IterateExternalIncentiveIds(func(incentiveId string) bool {
127 incentive, ok := incentivesResolver.Get(incentiveId)
128 if !ok {
129 return false
130 }
131
132 incentiveResolver := NewExternalIncentiveResolver(incentive)
133
134 // Check if incentive is active during this specific collection period
135 if !incentiveResolver.IsStarted(param.CurrentTime) {
136 return false
137 }
138
139 // External incentivized pool.
140 // Calculate reward for each warmup using per-incentive lastCollectTime
141 externalLastCollectTime := depositResolver.ExternalRewardLastCollectTime(incentiveId)
142 externalReward, externalPenalty := rewardState.calculateExternalReward(externalLastCollectTime, param.CurrentTime, incentive)
143
144 for i := range externalReward {
145 if externalReward[i] > 0 || externalPenalty[i] > 0 {
146 rewards[i].External[incentiveId] = externalReward[i]
147 rewards[i].ExternalPenalty[incentiveId] = externalPenalty[i]
148 }
149 }
150
151 rewardState.reset()
152
153 return false
154 })
155
156 return rewards
157}
158
159// calculates internal unclaimable reward for the pool
160func (s *stakerV1) processUnClaimableReward(poolPath string, endTimestamp int64) int64 {
161 pool, ok := s.getPools().Get(poolPath)
162 if !ok {
163 return 0
164 }
165 poolResolver := NewPoolResolver(pool)
166 return poolResolver.processUnclaimableReward(endTimestamp)
167}
168
169// update deposit's incentive list with new incentives created since last update
170func (s *stakerV1) getExternalIncentiveIdsBy(poolPath string, startTime, endTime int64) []string {
171 currentIncentiveIds := make([]string, 0)
172
173 incentivesByTime := s.getExternalIncentivesByCreationTime()
174
175 incentivesByTime.Iterate(startTime, endTime, func(_ int64, value any) bool {
176 // Value is a slice of incentive IDs (handles timestamp collisions)
177 poolIncentiveIds, ok := value.(*bptree.BPTree)
178 if !ok {
179 return false
180 }
181
182 incentiveIdsValue, exists := poolIncentiveIds.Get(poolPath)
183 if !exists {
184 return false
185 }
186
187 incentiveIds, ok := incentiveIdsValue.([]string)
188 if !ok {
189 return false
190 }
191
192 currentIncentiveIds = append(currentIncentiveIds, incentiveIds...)
193
194 return false
195 })
196
197 return currentIncentiveIds
198}
199
200// getInitialCollectTime determines the initial collection time for an incentive
201// by taking the maximum of the deposit's stake time and the incentive's start time.
202// This ensures rewards are only calculated from when both conditions are met:
203// - The position must be staked (deposit.stakeTime)
204// - The incentive must be active (incentive.startTimestamp)
205//
206// This function is used for lazy initialization when a position collects
207// from an incentive for the first time, avoiding the need to iterate through
208// all deposits when a new incentive is created.
209func getInitialCollectTime(deposit *sr.Deposit, incentive *sr.ExternalIncentive) int64 {
210 if deposit.StakeTime() > incentive.StartTimestamp() {
211 return deposit.StakeTime()
212 }
213 return incentive.StartTimestamp()
214}