reward_calculation_pool.gno
18.27 Kb ยท 571 lines
1package v1
2
3import (
4 "time"
5
6 "gno.land/p/nt/avl"
7 "gno.land/p/nt/ufmt"
8
9 i256 "gno.land/p/gnoswap/int256"
10 u256 "gno.land/p/gnoswap/uint256"
11
12 "gno.land/r/gnoswap/common"
13 sr "gno.land/r/gnoswap/staker"
14)
15
16var q128 = u256.MustFromDecimal("340282366920938463463374607431768211456")
17
18// Pools represents the global pool storage
19type Pools struct {
20 tree *avl.Tree // string poolPath -> pool
21}
22
23func NewPools() *Pools {
24 return &Pools{
25 tree: avl.NewTree(),
26 }
27}
28
29// Get returns the pool for the given poolPath
30func (self *Pools) Get(poolPath string) (*sr.Pool, bool) {
31 v, ok := self.tree.Get(poolPath)
32 if !ok {
33 return nil, false
34 }
35 p, ok := v.(*sr.Pool)
36 if !ok {
37 panic(ufmt.Sprintf("failed to cast v to *Pool: %T", v))
38 }
39 return p, true
40}
41
42// GetPoolOrNil returns the pool for the given poolPath, or returns nil if it does not exist
43func (self *Pools) GetPoolOrNil(poolPath string) *sr.Pool {
44 pool, ok := self.Get(poolPath)
45 if !ok {
46 return nil
47 }
48 return pool
49}
50
51// set sets the pool for the given poolPath
52func (self *Pools) set(poolPath string, pool *sr.Pool) {
53 self.tree.Set(poolPath, pool)
54}
55
56// Has returns true if the pool exists for the given poolPath
57func (self *Pools) Has(poolPath string) bool {
58 return self.tree.Has(poolPath)
59}
60
61func (self *Pools) IterateAll(fn func(key string, pool *sr.Pool) bool) {
62 self.tree.Iterate("", "", func(key string, value any) bool {
63 p, ok := value.(*sr.Pool)
64 if !ok {
65 panic(ufmt.Sprintf("failed to cast value to *Pool: %T", value))
66 }
67 return fn(key, p)
68 })
69}
70
71type PoolResolver struct {
72 *sr.Pool
73}
74
75func (self *PoolResolver) IncentivesResolver() *IncentivesResolver {
76 return NewIncentivesResolver(self.Incentives())
77}
78
79// Get the latest global reward ratio accumulation in [0, currentTime] range.
80// Returns the time and the accumulation.
81func (self *PoolResolver) CurrentGlobalRewardRatioAccumulation(currentTime int64) (time int64, acc *u256.Uint) {
82 acc = u256.Zero()
83
84 self.GlobalRewardRatioAccumulation().ReverseIterate(0, currentTime, func(key int64, value any) bool {
85 time = key
86 v, ok := value.(*u256.Uint)
87 if !ok {
88 panic(ufmt.Sprintf("failed to cast value to *u256.Uint: %T", value))
89 }
90 acc = v
91 return true
92 })
93 if acc == nil {
94 panic("should not happen, globalRewardRatioAccumulation must be set when pool is created")
95 }
96 return time, acc
97}
98
99// Get the latest tick in [0, currentTime] range.
100// Returns the tick.
101func (self *PoolResolver) CurrentTick(currentTime int64) (tick int32) {
102 self.HistoricalTick().ReverseIterate(0, currentTime, func(key int64, value any) bool {
103 res, ok := value.(int32)
104 if !ok {
105 panic(ufmt.Sprintf("failed to cast value to int32: %T", value))
106 }
107 tick = res
108 return true
109 })
110 return tick
111}
112
113func (self *PoolResolver) CurrentStakedLiquidity(currentTime int64) (liquidity *u256.Uint) {
114 liquidity = u256.Zero()
115
116 self.StakedLiquidity().ReverseIterate(0, currentTime, func(key int64, value any) bool {
117 res, ok := value.(*u256.Uint)
118 if !ok {
119 panic(ufmt.Sprintf("failed to cast value to *u256.Uint: %T", value))
120 }
121 liquidity = res
122 return true
123 })
124 return liquidity
125}
126
127func (self *PoolResolver) TickResolver(tickId int32) *TickResolver {
128 return NewTickResolver(self.Ticks().Get(tickId))
129}
130
131// IsExternallyIncentivizedPool returns true if the pool has any active external incentives.
132func (self *PoolResolver) IsExternallyIncentivizedPool() bool {
133 currentTime := time.Now().Unix()
134 hasIncentive := false
135 self.Incentives().IncentiveTrees().Iterate("", "", func(key string, value any) bool {
136 incentive, ok := value.(*sr.ExternalIncentive)
137 if !ok {
138 panic("failed to cast value to *ExternalIncentive")
139 }
140
141 resolver := NewExternalIncentiveResolver(incentive)
142 if !resolver.IsEnded(currentTime) {
143 hasIncentive = true
144 return true
145 }
146
147 return false
148 })
149
150 return hasIncentive
151}
152
153// Get the latest reward in [0, currentTime] range.
154// Returns the reward.
155func (self *PoolResolver) CurrentReward(currentTime int64) (reward int64) {
156 self.RewardCache().ReverseIterate(0, currentTime, func(key int64, value any) bool {
157 res, ok := value.(int64)
158 if !ok {
159 panic(ufmt.Sprintf("failed to cast value to int64: %T", value))
160 }
161 reward = res
162 return true
163 })
164 return reward
165}
166
167// cacheReward sets the current reward for the pool
168// If the pool is in unclaimable period, it will end the unclaimable period, updates the reward, and start the unclaimable period again.
169//
170// Important behavior for initial tier assignment:
171// - When a pool first receives a tier, oldTierReward=0 and currentTierReward>0
172// - If the pool has zero liquidity at this point, startUnclaimablePeriod() is called
173// - This ensures unclaimable period tracking begins from the moment rewards start emitting
174func (self *PoolResolver) cacheReward(currentTime int64, currentTierReward int64) {
175 oldTierReward := self.CurrentReward(currentTime)
176 if oldTierReward == currentTierReward {
177 return
178 }
179
180 isInUnclaimable := self.CurrentStakedLiquidity(currentTime).IsZero()
181 if isInUnclaimable {
182 // End any existing unclaimable period
183 // Note: If lastUnclaimableTime is 0 (not yet tracking), this is a no-op
184 self.endUnclaimablePeriod(currentTime)
185 }
186
187 self.RewardCache().Set(currentTime, currentTierReward)
188
189 if isInUnclaimable {
190 // Start/restart unclaimable period tracking
191 // This handles initial tier assignment when lastUnclaimableTime is 0
192 self.startUnclaimablePeriod(currentTime)
193 }
194}
195
196// cacheInternalReward caches the current emission and updates the global reward ratio accumulation.
197func (self *PoolResolver) cacheInternalReward(currentTime int64, currentEmission int64) {
198 self.cacheReward(currentTime, currentEmission)
199
200 currentStakedLiquidity := self.CurrentStakedLiquidity(currentTime)
201 self.updateGlobalRewardRatioAccumulation(currentTime, currentStakedLiquidity)
202}
203
204func (self *PoolResolver) calculateGlobalRewardRatioAccumulation(currentTime int64, currentStakedLiquidity *u256.Uint) *u256.Uint {
205 oldAccTime, oldAcc := self.CurrentGlobalRewardRatioAccumulation(currentTime)
206 timeDiff := safeSubInt64(currentTime, oldAccTime)
207 if timeDiff == 0 {
208 return oldAcc.Clone()
209 }
210 if timeDiff < 0 {
211 panic("time cannot go backwards")
212 }
213
214 if currentStakedLiquidity.IsZero() {
215 return oldAcc.Clone()
216 }
217
218 acc := u256.MulDiv(
219 u256.NewUintFromInt64(timeDiff),
220 q128,
221 currentStakedLiquidity,
222 )
223 return u256.Zero().Add(oldAcc, acc)
224}
225
226// updateGlobalRewardRatioAccumulation updates the global reward ratio accumulation and returns the new accumulation.
227func (self *PoolResolver) updateGlobalRewardRatioAccumulation(currentTime int64, currentStakedLiquidity *u256.Uint) *u256.Uint {
228 newAcc := self.calculateGlobalRewardRatioAccumulation(currentTime, currentStakedLiquidity)
229
230 self.GlobalRewardRatioAccumulation().Set(currentTime, newAcc)
231 return newAcc
232}
233
234// RewardStateOf initializes a new RewardState for the given deposit.
235func (self *PoolResolver) RewardStateOf(deposit *sr.Deposit) *RewardState {
236 warmups := len(deposit.Warmups())
237 result := &RewardState{
238 pool: self,
239 deposit: NewDepositResolver(deposit),
240 rewards: make([]int64, warmups),
241 penalties: make([]int64, warmups),
242 }
243
244 for i := range result.rewards {
245 result.rewards[i] = 0
246 result.penalties[i] = 0
247 }
248
249 return result
250}
251
252// reset clears cached rewards/penalties so a RewardState can be reused without re-allocating.
253func (self *RewardState) reset() {
254 for i := range self.rewards {
255 self.rewards[i] = 0
256 self.penalties[i] = 0
257 }
258}
259
260// NewPool creates a new pool with the given poolPath and currentHeight.
261func NewPoolResolver(pool *sr.Pool) *PoolResolver {
262 return &PoolResolver{
263 Pool: pool,
264 }
265}
266
267// RewardState is a struct for storing the intermediate state for reward calculation.
268type RewardState struct {
269 pool *PoolResolver
270 deposit *DepositResolver
271
272 // accumulated rewards for each warmup
273 rewards []int64
274 penalties []int64
275}
276
277// calculateInternalReward calculates the internal reward for the deposit.
278// It calls rewardPerWarmup for each rewardCache interval, applies warmup, and returns the rewards and penalties.
279func (self *RewardState) calculateInternalReward(startTime, endTime int64) ([]int64, []int64) {
280 currentReward := self.pool.CurrentReward(startTime)
281
282 self.pool.RewardCache().Iterate(startTime, endTime, func(key int64, value any) bool {
283 reward, ok := value.(int64)
284 if !ok {
285 panic(ufmt.Sprintf("failed to cast value to int64: %T", value))
286 }
287
288 // Calculate reward for the period before this cache entry
289 err := self.rewardPerWarmup(startTime, key, currentReward)
290 if err != nil {
291 panic(err)
292 }
293
294 startTime = key
295 currentReward = reward
296 return false
297 })
298
299 if startTime < endTime {
300 // For the remaining period, use the last cached reward value
301 // If the pool was de-tiered and the last cached value is 0, this ensures no new rewards
302 err := self.rewardPerWarmup(startTime, endTime, currentReward)
303 if err != nil {
304 panic(err)
305 }
306 }
307
308 self.applyWarmup()
309
310 return self.rewards, self.penalties
311}
312
313// updateExternalReward updates the external reward for the deposit.
314// It updates the last collect time for the external reward for the given incentive ID.
315// It returns an error if the current time is less than the last collect time for the external reward for the given incentive ID.
316func (self *RewardState) updateExternalReward(startTime, endTime int64, incentive *sr.ExternalIncentive) error {
317 lastCollectTime := self.deposit.ExternalRewardLastCollectTime(incentive.IncentiveId())
318 if startTime < lastCollectTime {
319 // This must not happen, but adding some guards just in case.
320 startTime = lastCollectTime
321 }
322
323 ictvStart := incentive.StartTimestamp()
324 if endTime < ictvStart {
325 return nil // Not started yet
326 }
327
328 if startTime < ictvStart {
329 startTime = ictvStart
330 }
331
332 ictvEnd := incentive.EndTimestamp()
333 if endTime > ictvEnd {
334 endTime = ictvEnd
335 }
336
337 if startTime > ictvEnd {
338 return nil // Already ended
339 }
340
341 return self.rewardPerWarmup(startTime, endTime, incentive.RewardPerSecond())
342}
343
344// calculateCollectableExternalReward calculates the calculated external reward for the deposit.
345// It calls updateExternalReward for the incentive period, applies warmup and returns the rewards and penalties.
346// used for reward calculation for a calculatable incentive
347func (self *RewardState) calculateCollectableExternalReward(startTime, endTime int64, incentive *sr.ExternalIncentive) int64 {
348 err := self.updateExternalReward(startTime, endTime, incentive)
349 if err != nil {
350 panic(err)
351 }
352
353 currentReward := u256.Zero()
354
355 for i := range self.rewards {
356 currentReward = currentReward.Add(currentReward, u256.NewUintFromInt64(self.rewards[i]))
357 }
358
359 return safeConvertToInt64(currentReward)
360}
361
362// calculateExternalReward calculates the external reward for the deposit.
363// It calls rewardPerWarmup for startTime to endTime(clamped to the incentive period), applies warmup and returns the rewards and penalties.
364func (self *RewardState) calculateExternalReward(startTime, endTime int64, incentive *sr.ExternalIncentive) ([]int64, []int64) {
365 err := self.updateExternalReward(startTime, endTime, incentive)
366 if err != nil {
367 panic(err)
368 }
369
370 // apply warmup to collect rewards
371 self.applyWarmup()
372
373 return self.rewards, self.penalties
374}
375
376// applyWarmup applies the warmup to the rewards and calculate penalties.
377func (self *RewardState) applyWarmup() {
378 for i, warmup := range self.deposit.Warmups() {
379 warmupReward := self.rewards[i]
380
381 // calculate warmup reward applying warmup ratio
382 self.rewards[i] = safeMulInt64(warmupReward, int64(warmup.WarmupRatio)) / 100
383
384 // warmup penalty is the difference between the warmup reward and the warmup reward applying warmup ratio
385 self.penalties[i] = safeSubInt64(warmupReward, self.rewards[i])
386 }
387}
388
389// rewardPerWarmup calculates the reward for each warmup, adds to the RewardState's rewards array.
390func (self *RewardState) rewardPerWarmup(startTime, endTime int64, rewardPerSecond int64) error {
391 // Return early if startTime equals endTime to avoid unnecessary computation
392 if startTime == endTime {
393 return nil
394 }
395
396 for i, warmup := range self.deposit.Warmups() {
397 if startTime >= warmup.NextWarmupTime {
398 // passed the warmup
399 continue
400 }
401
402 if endTime < warmup.NextWarmupTime {
403 rewardAcc := self.pool.CalculateRewardForPosition(
404 startTime,
405 self.pool.CurrentTick(startTime),
406 endTime,
407 self.pool.CurrentTick(endTime),
408 self.deposit.Deposit,
409 )
410
411 rewardAcc = u256.Zero().Mul(rewardAcc, self.deposit.Liquidity())
412 rewardAcc = u256.MulDiv(rewardAcc, u256.NewUintFromInt64(rewardPerSecond), q128)
413 self.rewards[i] = safeAddInt64(self.rewards[i], safeConvertToInt64(rewardAcc))
414
415 break
416 }
417
418 rewardAcc := self.pool.CalculateRewardForPosition(
419 startTime,
420 self.pool.CurrentTick(startTime),
421 warmup.NextWarmupTime,
422 self.pool.CurrentTick(warmup.NextWarmupTime),
423 self.deposit.Deposit,
424 )
425
426 rewardAcc = u256.Zero().Mul(rewardAcc, self.deposit.Liquidity())
427 rewardAcc = u256.MulDiv(rewardAcc, u256.NewUintFromInt64(rewardPerSecond), q128)
428 self.rewards[i] = safeAddInt64(self.rewards[i], safeConvertToInt64(rewardAcc))
429
430 startTime = warmup.NextWarmupTime
431 }
432
433 return nil
434}
435
436// modifyDeposit updates the pool's staked liquidity and returns the new staked liquidity.
437// updates when there is a change in the staked liquidity(tick cross, stake, unstake)
438func (self *PoolResolver) modifyDeposit(delta *i256.Int, currentTime int64, nextTick int32) *u256.Uint {
439 // update staker side pool info
440 lastStakedLiquidity := self.CurrentStakedLiquidity(currentTime)
441 deltaApplied := common.LiquidityMathAddDelta(lastStakedLiquidity, delta)
442 result := self.updateGlobalRewardRatioAccumulation(currentTime, lastStakedLiquidity)
443
444 // historical tick does NOT actually reflect the tick at the timestamp, but it provides correct ordering for the staked positions
445 // because TickCrossHook is assured to be called for the staked-initialized ticks
446 self.HistoricalTick().Set(currentTime, nextTick)
447
448 switch deltaApplied.Sign() {
449 case -1:
450 panic("stakedLiquidity is less than 0, should not happen")
451 case 0:
452 if lastStakedLiquidity.Sign() == 1 {
453 // StakedLiquidity moved from positive to zero, start unclaimable period
454 self.startUnclaimablePeriod(currentTime)
455 self.IncentivesResolver().startUnclaimablePeriod(currentTime)
456 }
457 case 1:
458 if lastStakedLiquidity.Sign() == 0 {
459 // StakedLiquidity moved from zero to positive, end unclaimable period
460 self.endUnclaimablePeriod(currentTime)
461 self.IncentivesResolver().endUnclaimablePeriod(currentTime)
462 }
463 }
464
465 self.Pool.StakedLiquidity().Set(currentTime, deltaApplied)
466
467 return result
468}
469
470// startUnclaimablePeriod starts the unclaimable period.
471func (self *PoolResolver) startUnclaimablePeriod(currentTime int64) {
472 if self.LastUnclaimableTime() == 0 {
473 // We set only if it's the first time entering(0 indicates not set yet)
474 self.SetLastUnclaimableTime(currentTime)
475 }
476}
477
478// endUnclaimablePeriod ends the unclaimable period.
479// Accumulates to unclaimableAcc and resets lastUnclaimableTime to 0.
480func (self *PoolResolver) endUnclaimablePeriod(currentTime int64) {
481 if self.LastUnclaimableTime() == 0 {
482 // lastUnclaimableTime = 0 means tracking hasn't started yet
483 // This is normal during initial pool creation or when called from cacheReward
484 // during tier assignment with zero liquidity
485 return
486 }
487
488 self.updateUnclaimableAccumulateRewards(currentTime)
489 self.SetLastUnclaimableTime(0)
490}
491
492// updateUnclaimableAccumulateRewards ends the unclaimable period.
493// Accumulates to unclaimableAcc and resets lastUnclaimableTime to 0.
494func (self *PoolResolver) updateUnclaimableAccumulateRewards(currentTime int64) {
495 if self.LastUnclaimableTime() >= currentTime {
496 return
497 }
498
499 unclaimableDuration := safeSubInt64(currentTime, self.LastUnclaimableTime())
500 currentUnclaimableReward := safeMulInt64(unclaimableDuration, self.CurrentReward(self.LastUnclaimableTime()))
501 self.SetUnclaimableAcc(safeAddInt64(self.UnclaimableAcc(), currentUnclaimableReward))
502}
503
504// processUnclaimableReward processes the unclaimable reward and returns the accumulated reward.
505// It resets unclaimableAcc to 0 and properly manages lastUnclaimableTime based on pool state.
506func (self *PoolResolver) processUnclaimableReward(endTime int64) int64 {
507 // Check current pool liquidity state
508 isZeroStakedLiquidity := self.CurrentStakedLiquidity(endTime).IsZero()
509
510 if self.LastUnclaimableTime() > 0 {
511 // We have an ongoing unclaimable period tracking
512 self.updateUnclaimableAccumulateRewards(endTime)
513
514 if isZeroStakedLiquidity {
515 // Still unclaimable - accumulate rewards up to endTime
516 // Update tracking time for continuing unclaimable period
517 self.SetLastUnclaimableTime(endTime)
518 } else {
519 // Was unclaimable but now has liquidity - properly end the period
520 self.SetLastUnclaimableTime(0)
521 }
522 } else {
523 if isZeroStakedLiquidity {
524 // No previous tracking but currently unclaimable - this shouldn't normally happen
525 // as startUnclaimablePeriod should have been called when liquidity reached 0
526 // Start tracking from now
527 self.SetLastUnclaimableTime(endTime)
528 }
529 }
530
531 // Return and reset accumulated unclaimable rewards
532 internalUnClaimable := self.UnclaimableAcc()
533 self.SetUnclaimableAcc(0)
534 return internalUnClaimable
535}
536
537// Calculates reward for a position *without* considering debt or warmup
538// It calculates the theoretical total reward for the position if it has been staked since the pool creation
539func (self *PoolResolver) CalculateRawRewardForPosition(currentTime int64, currentTick int32, deposit *sr.Deposit) *u256.Uint {
540 var rewardAcc *u256.Uint
541
542 globalAcc := self.calculateGlobalRewardRatioAccumulation(currentTime, self.CurrentStakedLiquidity(currentTime))
543
544 lowerAcc := self.TickResolver(deposit.TickLower()).CurrentOutsideAccumulation(currentTime)
545 upperAcc := self.TickResolver(deposit.TickUpper()).CurrentOutsideAccumulation(currentTime)
546 if currentTick < deposit.TickLower() {
547 rewardAcc = u256.Zero().Sub(lowerAcc, upperAcc)
548 } else if currentTick >= deposit.TickUpper() {
549 rewardAcc = u256.Zero().Sub(upperAcc, lowerAcc)
550 } else {
551 rewardAcc = u256.Zero().Sub(globalAcc, lowerAcc)
552 rewardAcc = rewardAcc.Sub(rewardAcc, upperAcc)
553 }
554
555 return rewardAcc
556}
557
558// Calculate actual reward in [startTime, endTime) for a position by
559// subtracting the startTime's raw reward from the endTime's raw reward
560func (self *PoolResolver) CalculateRewardForPosition(
561 startTime int64,
562 startTick int32,
563 endTime int64,
564 endTick int32,
565 deposit *sr.Deposit,
566) *u256.Uint {
567 rewardAcc := self.CalculateRawRewardForPosition(endTime, endTick, deposit)
568 debtAcc := self.CalculateRawRewardForPosition(startTime, startTick, deposit)
569
570 return u256.Zero().Sub(rewardAcc, debtAcc)
571}