Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}