reward_calculation_pool.gno
27.85 Kb · 784 lines
1package staker
2
3import (
4 "errors"
5 "math"
6 "time"
7
8 "gno.land/p/gnoswap/gnsmath/v1"
9 bptree "gno.land/p/nt/bptree/v0"
10 ufmt "gno.land/p/nt/ufmt/v0"
11
12 i256 "gno.land/p/gnoswap/int256/v1"
13 u256 "gno.land/p/gnoswap/uint256/v1"
14 sr "gno.land/r/gnoswap/staker"
15)
16
17var q128 = u256.MustFromDecimal("340282366920938463463374607431768211456")
18
19// Pools represents the global pool storage
20type Pools struct {
21 tree *bptree.BPTree // string poolPath -> pool
22}
23
24// NewPools creates an empty resolver for global pool storage.
25//
26// Returns:
27// - pools: Pools resolver backed by a new B+ tree keyed by pool path.
28func NewPools() *Pools {
29 return &Pools{
30 tree: sr.NewBPTreeN(16),
31 }
32}
33
34// Get returns the pool stored under the given pool path.
35//
36// Parameters:
37// - poolPath: Pool path used as the storage key.
38//
39// Returns:
40// - pool: Stored pool pointer when poolPath exists; nil when absent.
41// - found: True when poolPath resolves to a pool; false when no entry exists.
42func (self *Pools) Get(poolPath string) (*sr.Pool, bool) {
43 v := self.tree.Get(poolPath)
44 if v == nil {
45 return nil, false
46 }
47 p, ok := v.(*sr.Pool)
48 if !ok {
49 panic(ufmt.Sprintf("failed to cast v to *Pool: %T", v))
50 }
51 return p, true
52}
53
54// GetPoolOrNil returns the pool for the given pool path, or nil when it does not exist.
55//
56// Parameters:
57// - poolPath: Pool path used as the storage key.
58//
59// Returns:
60// - pool: Stored pool pointer, or nil when poolPath is absent.
61func (self *Pools) GetPoolOrNil(poolPath string) *sr.Pool {
62 pool, ok := self.Get(poolPath)
63 if !ok {
64 return nil
65 }
66 return pool
67}
68
69// set sets the pool for the given poolPath.
70func (self *Pools) set(poolPath string, pool *sr.Pool) {
71 self.tree.Set(poolPath, pool)
72}
73
74// Has reports whether a pool exists for the given pool path.
75//
76// Parameters:
77// - poolPath: Pool path used as the storage key.
78//
79// Returns:
80// - exists: True when poolPath is present in the pool tree; false otherwise.
81func (self *Pools) Has(poolPath string) bool {
82 return self.tree.Has(poolPath)
83}
84
85// IterateAll visits every stored pool until the callback requests that traversal stop.
86//
87// Parameters:
88// - fn: Callback receiving each pool path and pool pointer; return true to stop iteration, false to continue.
89func (self *Pools) IterateAll(fn func(key string, pool *sr.Pool) bool) {
90 self.tree.Iterate("", "", func(key string, value any) bool {
91 p, ok := value.(*sr.Pool)
92 if !ok {
93 panic(ufmt.Sprintf("failed to cast value to *Pool: %T", value))
94 }
95 return fn(key, p)
96 })
97}
98
99type PoolResolver struct {
100 *sr.Pool
101
102 // exit is set only when collecting an exit checkpoint, and overrides the pool reads that
103 // would otherwise come from state the position no longer takes part in.
104 exit *sr.UnstakedPosition
105}
106
107// IncentivesResolver returns a resolver over the pool's external-incentive state.
108//
109// Returns:
110// - resolver: Incentives resolver backed by this pool's incentive tree and unclaimable periods.
111func (self *PoolResolver) IncentivesResolver() *IncentivesResolver {
112 return NewIncentivesResolver(self.Incentives())
113}
114
115// CurrentGlobalRewardRatioAccumulation returns the latest stored global reward-ratio checkpoint in the [0, currentTime] range.
116//
117// Parameters:
118// - currentTime: Unix timestamp bounding the checkpoint lookup.
119//
120// Returns:
121// - time: Timestamp of the latest stored checkpoint at or before currentTime; zero when no checkpoint exists.
122// - acc: Decimal-encoded Q128-scaled global reward-ratio accumulation at time.
123func (self *PoolResolver) CurrentGlobalRewardRatioAccumulation(currentTime int64) (time int64, acc string) {
124 acc = "0"
125
126 self.GlobalRewardRatioAccumulation().ReverseIterate(0, currentTime, func(key int64, value any) bool {
127 time = key
128
129 valueStr, ok := value.(string)
130 if !ok {
131 panic(ufmt.Sprintf("failed to cast value to string: %T", value))
132 }
133
134 acc = valueStr
135
136 return true
137 })
138
139 return time, acc
140}
141
142// CurrentTick returns the latest historical tick in the [0, currentTime] range.
143//
144// Parameters:
145// - currentTime: Unix timestamp bounding the historical-tick lookup.
146//
147// Returns:
148// - tick: Latest historical tick at or before currentTime; an exit resolver uses its pinned exit tick at or after exit time.
149func (self *PoolResolver) CurrentTick(currentTime int64) (tick int32) {
150 if self.exit != nil && currentTime >= self.exit.ExitTime() {
151 return self.exit.ExitTick()
152 }
153
154 self.HistoricalTick().ReverseIterate(0, currentTime, func(key int64, value any) bool {
155 res, ok := value.(int32)
156 if !ok {
157 panic(ufmt.Sprintf("failed to cast value to int32: %T", value))
158 }
159 tick = res
160 return true
161 })
162 return tick
163}
164
165// CurrentStakedLiquidity returns the latest staked-liquidity checkpoint in the [0, currentTime] range.
166//
167// Parameters:
168// - currentTime: Unix timestamp bounding the staked-liquidity lookup.
169//
170// Returns:
171// - liquidity: Uint256 staked liquidity effective at or before currentTime; zero when no checkpoint exists.
172func (self *PoolResolver) CurrentStakedLiquidity(currentTime int64) (liquidity *u256.Uint) {
173 liquidity = u256.Zero()
174
175 self.StakedLiquidity().ReverseIterate(0, currentTime, func(key int64, value any) bool {
176 res, ok := value.(*u256.Uint)
177 if !ok {
178 panic(ufmt.Sprintf("failed to cast value to *u256.Uint: %T", value))
179 }
180 liquidity = res
181 return true
182 })
183 return liquidity
184}
185
186// GetOrNewTick returns the existing tick or a new zero-valued tick.
187//
188// Parameters:
189// - tickId: Boundary tick identifier to retrieve or initialize.
190//
191// Returns:
192// - tick: Existing stored tick, or a zero-valued tick with tickId when no entry exists.
193//
194// Substituting a zero-valued tick on a read is safe because ticks are pruned
195// only when their staked gross liquidity reaches zero, in the same call that
196// removes the last deposit referencing them.
197func (self *PoolResolver) GetOrNewTick(tickId int32) *sr.Tick {
198 if self.exit != nil {
199 if lowerTick := self.exit.LowerTick(); lowerTick != nil && lowerTick.Id() == tickId {
200 return lowerTick
201 }
202 if upperTick := self.exit.UpperTick(); upperTick != nil && upperTick.Id() == tickId {
203 return upperTick
204 }
205 }
206
207 tick := self.Ticks().Get(tickId)
208 if tick == nil {
209 return sr.NewTick(tickId)
210 }
211 return tick
212}
213
214// initializeNewTickOutsideAccumulation seeds a just-created boundary tick so
215// that below(tick) == globalAccumulation holds, keeping the three branches of
216// outsideAccumulationAt returns a boundary tick's outside accumulation at currentTime.
217//
218// An exit checkpoint answers for its own exit timestamp from the value pinned when it was
219// written: a tick cross later in the same block overwrites the entry at that timestamp, while
220// earlier entries can no longer change.
221func (self *PoolResolver) outsideAccumulationAt(tickId int32, currentTime int64) *u256.Uint {
222 if self.exit != nil && currentTime == self.exit.ExitTime() {
223 switch tickId {
224 case self.exit.Deposit().TickLower():
225 return u256.MustFromDecimal(self.exit.LowerOutsideAcc())
226 case self.exit.Deposit().TickUpper():
227 return u256.MustFromDecimal(self.exit.UpperOutsideAcc())
228 }
229 }
230
231 return NewTickResolver(self.GetOrNewTick(tickId)).CurrentOutsideAccumulation(currentTime)
232}
233
234// CalculateRawRewardForPosition consistent.
235func (self *PoolResolver) initializeNewTickOutsideAccumulation(currentTime int64, currentTick, tickId int32, tick *sr.Tick) {
236 if tickId > currentTick {
237 return
238 }
239
240 globalAcc, _ := self.globalRewardRatioAccumulationAt(currentTime)
241 tick.SetOutsideAccumulationAt(currentTime, globalAcc)
242}
243
244// IsExternallyIncentivizedPool reports whether the pool has any external incentive that has not ended,
245// including incentives whose start time is still in the future.
246//
247// Returns:
248// - incentivized: True when at least one non-ended external incentive is indexed for the pool; false when all are ended or none exist.
249func (self *PoolResolver) IsExternallyIncentivizedPool() bool {
250 currentTime := time.Now().Unix()
251 hasIncentive := false
252 // With a maximum duration of 365 days, older starts have already ended.
253 // Keep future starts eligible and retain historical records for reward claims.
254 self.Incentives().IterateIncentiveIdsByTime(stakeScanLowerBound(currentTime), math.MaxInt64, func(incentiveId string) bool {
255 incentive, ok := self.Incentives().Incentive(incentiveId)
256 if !ok {
257 panic("incentive missing from pool start-time index")
258 }
259
260 resolver := NewExternalIncentiveResolver(incentive)
261 if !resolver.IsEnded(currentTime) {
262 hasIncentive = true
263 return true
264 }
265
266 return false
267 })
268
269 return hasIncentive
270}
271
272// CurrentReward returns the latest cached per-pool reward rate in the [0, currentTime] range.
273//
274// Parameters:
275// - currentTime: Unix timestamp bounding the reward-cache lookup.
276//
277// Returns:
278// - reward: Latest cached GNS reward rate at or before currentTime, in units per second; zero when no checkpoint exists.
279func (self *PoolResolver) CurrentReward(currentTime int64) (reward int64) {
280 self.RewardCache().ReverseIterate(0, currentTime, func(key int64, value any) bool {
281 res, ok := value.(int64)
282 if !ok {
283 panic(ufmt.Sprintf("failed to cast value to int64: %T", value))
284 }
285 reward = res
286 return true
287 })
288 return reward
289}
290
291func (self *PoolResolver) isChangedTick(currentTime int64, currentTick int32) bool {
292 if self.HistoricalTick().Size() == 0 {
293 return true
294 }
295
296 previousTick := self.CurrentTick(currentTime)
297
298 return previousTick != currentTick
299}
300
301// cacheReward sets the current reward for the pool
302// If the pool is in unclaimable period, it will end the unclaimable period, updates the reward, and start the unclaimable period again.
303//
304// Important behavior for initial tier assignment:
305// - When a pool first receives a tier, oldTierReward=0 and currentTierReward>0
306// - If the pool has zero liquidity at this point, startUnclaimablePeriod() is called
307// - This ensures unclaimable period tracking begins from the moment rewards start emitting
308func (self *PoolResolver) cacheReward(currentTime int64, currentTierReward int64) {
309 oldTierReward := self.CurrentReward(currentTime)
310 if oldTierReward == currentTierReward {
311 return
312 }
313
314 isInUnclaimable := self.CurrentStakedLiquidity(currentTime).IsZero()
315 if isInUnclaimable {
316 // End any existing unclaimable period
317 // Note: If lastUnclaimableTime is 0 (not yet tracking), this is a no-op
318 self.endUnclaimablePeriod(currentTime)
319 }
320
321 self.Pool.SetRewardCacheAt(currentTime, currentTierReward)
322
323 if isInUnclaimable {
324 // Start/restart unclaimable period tracking
325 // This handles initial tier assignment when lastUnclaimableTime is 0
326 self.startUnclaimablePeriod(currentTime)
327 }
328}
329
330func (self *PoolResolver) calculateGlobalRewardRatioAccumulation(currentTime int64, currentStakedLiquidity *u256.Uint) *u256.Uint {
331 oldAccTime, oldAccStr := self.CurrentGlobalRewardRatioAccumulation(currentTime)
332 timeDiff := gnsmath.SafeSubInt64(currentTime, oldAccTime)
333 if timeDiff == 0 {
334 return u256.MustFromDecimal(oldAccStr)
335 }
336 if timeDiff < 0 {
337 panic("time cannot go backwards")
338 }
339
340 if currentStakedLiquidity.IsZero() {
341 return u256.MustFromDecimal(oldAccStr)
342 }
343
344 oldAcc := u256.MustFromDecimal(oldAccStr)
345 acc := u256.MulDiv(
346 u256.NewUintFromInt64(timeDiff),
347 q128,
348 currentStakedLiquidity,
349 )
350 return u256.Zero().Add(oldAcc, acc)
351}
352
353// globalRewardRatioAccumulationAt returns the global reward ratio accumulation *at* currentTime.
354//
355// CurrentGlobalRewardRatioAccumulation returns the latest stored checkpoint (<= currentTime), which is
356// only equal to the accumulation at currentTime when a checkpoint was written at that very timestamp.
357// Checkpoints are written exclusively by modifyDeposit (staked liquidity changes), so on any other path
358// the stored value lags by (currentTime - lastCheckpointTime) * q128 / stakedLiquidity.
359//
360// Reward calculation never has this problem because it derives the accumulation on demand
361// (CalculateRawRewardForPosition). Event emission must do the same, otherwise off-chain indexers that
362// treat the emitted accumulator as authoritative at the event timestamp silently drop that interval.
363func (self *PoolResolver) globalRewardRatioAccumulationAt(currentTime int64) (*u256.Uint, *u256.Uint) {
364 stakedLiquidity := self.CurrentStakedLiquidity(currentTime)
365 accumulation := self.calculateGlobalRewardRatioAccumulation(currentTime, stakedLiquidity)
366
367 return accumulation, stakedLiquidity
368}
369
370// updateGlobalRewardRatioAccumulation updates the global reward ratio accumulation and returns the new accumulation.
371func (self *PoolResolver) updateGlobalRewardRatioAccumulation(currentTime int64, currentStakedLiquidity *u256.Uint) *u256.Uint {
372 newAcc := self.calculateGlobalRewardRatioAccumulation(currentTime, currentStakedLiquidity)
373
374 // Persist as string to reduce stored object complexity.
375 self.Pool.SetGlobalRewardRatioAccumulationAt(currentTime, newAcc.ToString())
376 return newAcc
377}
378
379// RewardStateOf initializes a new RewardState for the given deposit, allocating reward and penalty slots for each warmup.
380//
381// Parameters:
382// - deposit: Staked deposit whose pool, liquidity, and warmup schedule will be resolved.
383//
384// Returns:
385// - state: RewardState initialized with zeroed per-warmup reward and penalty accumulators.
386func (self *PoolResolver) RewardStateOf(deposit *sr.Deposit) *RewardState {
387 warmups := len(deposit.Warmups())
388 result := &RewardState{
389 pool: self,
390 deposit: NewDepositResolver(deposit),
391 rewards: make([]int64, warmups),
392 penalties: make([]int64, warmups),
393 }
394
395 return result
396}
397
398// reset clears cached rewards/penalties so a RewardState can be reused without re-allocating.
399func (self *RewardState) reset() {
400 for i := range self.rewards {
401 self.rewards[i] = 0
402 self.penalties[i] = 0
403 }
404}
405
406// NewPoolResolver wraps a pool's persisted reward, liquidity, tick, and incentive state for calculations.
407//
408// Parameters:
409// - pool: Pool state to resolve.
410//
411// Returns:
412// - resolver: Pool resolver backed by pool.
413func NewPoolResolver(pool *sr.Pool) *PoolResolver {
414 return &PoolResolver{
415 Pool: pool,
416 }
417}
418
419// newPoolResolverWithExit builds a resolver that reads the pool through an exit checkpoint.
420func newPoolResolverWithExit(pool *sr.Pool, exit *sr.UnstakedPosition) *PoolResolver {
421 return &PoolResolver{
422 Pool: pool,
423 exit: exit,
424 }
425}
426
427// RewardState is a struct for storing the intermediate state for reward calculation.
428type RewardState struct {
429 pool *PoolResolver
430 deposit *DepositResolver
431
432 // accumulated rewards for each warmup
433 rewards []int64
434 penalties []int64
435}
436
437// calculateInternalReward computes the position's per-warmup rewards and penalties from a pre-resolved
438// per-second reward-rate schedule (see PoolResolver.resolveInternalRewardSegments).
439//
440// It is pure: it neither queries pool tier/emission state nor writes any state, so the read-only view
441// path and the collect path use it identically. Each segment [start, end) is applied at its constant
442// per-second rate; rewardPerWarmup is a no-op for empty segments (start == end).
443func (self *RewardState) calculateInternalReward(segments []internalRewardSegment) ([]int64, []int64) {
444 for _, seg := range segments {
445 if err := self.rewardPerWarmup(seg.start, seg.end, seg.rewardPerSecond); err != nil {
446 panic(err)
447 }
448 }
449
450 self.applyWarmup()
451
452 return self.rewards, self.penalties
453}
454
455// updateExternalReward updates the external reward for the deposit.
456// It updates the last collect time for the external reward for the given incentive ID.
457// It returns an error if the current time is less than the last collect time for the external reward for the given incentive ID.
458func (self *RewardState) updateExternalReward(startTime, endTime int64, incentive *sr.ExternalIncentive) error {
459 lastCollectTime := self.deposit.ExternalRewardLastCollectTime(incentive.IncentiveId())
460 if startTime < lastCollectTime {
461 // This must not happen, but adding some guards just in case.
462 startTime = lastCollectTime
463 }
464
465 ictvStart := incentive.StartTimestamp()
466 if endTime < ictvStart {
467 return nil // Not started yet
468 }
469
470 if startTime < ictvStart {
471 startTime = ictvStart
472 }
473
474 ictvEnd := incentive.EndTimestamp()
475 if endTime > ictvEnd {
476 endTime = ictvEnd
477 }
478
479 if startTime > ictvEnd {
480 return nil // Already ended
481 }
482
483 return self.rewardPerWarmupX128(startTime, endTime, incentive.RewardPerSecondX128())
484}
485
486// calculateCollectableExternalReward calculates the calculated external reward for the deposit.
487// It calls updateExternalReward for the incentive period, applies warmup and returns the rewards and penalties.
488// used for reward calculation for a calculatable incentive
489func (self *RewardState) calculateCollectableExternalReward(startTime, endTime int64, incentive *sr.ExternalIncentive) int64 {
490 err := self.updateExternalReward(startTime, endTime, incentive)
491 if err != nil {
492 panic(err)
493 }
494
495 currentReward := u256.Zero()
496
497 for i := range self.rewards {
498 currentReward = currentReward.Add(currentReward, u256.NewUintFromInt64(self.rewards[i]))
499 }
500
501 return gnsmath.SafeConvertToInt64(currentReward)
502}
503
504// calculateExternalReward calculates the external reward for the deposit.
505// It calls rewardPerWarmup for startTime to endTime(clamped to the incentive period), applies warmup and returns the rewards and penalties.
506func (self *RewardState) calculateExternalReward(startTime, endTime int64, incentive *sr.ExternalIncentive) ([]int64, []int64) {
507 err := self.updateExternalReward(startTime, endTime, incentive)
508 if err != nil {
509 panic(err)
510 }
511
512 // apply warmup to collect rewards
513 self.applyWarmup()
514
515 return self.rewards, self.penalties
516}
517
518// applyWarmup applies the warmup to the rewards and calculate penalties.
519func (self *RewardState) applyWarmup() {
520 for i, warmup := range self.deposit.Warmups() {
521 warmupReward := self.rewards[i]
522
523 // calculate warmup reward applying warmup ratio
524 self.rewards[i] = gnsmath.SafeMulDivInt64(warmupReward, int64(warmup.WarmupRatio), 100)
525
526 // warmup penalty is the difference between the warmup reward and the warmup reward applying warmup ratio
527 self.penalties[i] = gnsmath.SafeSubInt64(warmupReward, self.rewards[i])
528 }
529}
530
531// rewardPerWarmup calculates the reward for each warmup, adds to the RewardState's rewards array.
532// Used by the internal reward path where rewardPerSecond is an int64 emission rate.
533func (self *RewardState) rewardPerWarmup(startTime, endTime int64, rewardPerSecond int64) error {
534 // Return early if startTime equals endTime to avoid unnecessary computation
535 if startTime == endTime {
536 return nil
537 }
538
539 startTick := self.pool.CurrentTick(startTime)
540 startRaw := self.pool.CalculateRawRewardForPosition(startTime, startTick, self.deposit.Deposit)
541
542 for i, warmup := range self.deposit.Warmups() {
543 if startTime >= warmup.NextWarmupTime {
544 // passed the warmup
545 continue
546 }
547
548 if endTime < warmup.NextWarmupTime {
549 endTick := self.pool.CurrentTick(endTime)
550 endRaw := self.pool.CalculateRawRewardForPosition(endTime, endTick, self.deposit.Deposit)
551 // Modular by design: boundary ticks created at different times give a
552 // wrapped base, so the borrow is expected and the wrapped difference
553 // is the true accumulation (Uniswap V3 subtracts unchecked too).
554 rewardAcc := u256.Zero().Sub(endRaw, startRaw)
555
556 rewardAcc, overflow := u256.Zero().MulOverflow(rewardAcc, self.deposit.Liquidity())
557 if overflow {
558 panic(errors.New(errOverflow))
559 }
560
561 rewardAcc = u256.MulDiv(rewardAcc, u256.NewUintFromInt64(rewardPerSecond), q128)
562 self.rewards[i] = gnsmath.SafeAddInt64(self.rewards[i], gnsmath.SafeConvertToInt64(rewardAcc))
563
564 break
565 }
566
567 endTick := self.pool.CurrentTick(warmup.NextWarmupTime)
568 endRaw := self.pool.CalculateRawRewardForPosition(warmup.NextWarmupTime, endTick, self.deposit.Deposit)
569 // See the note above: the subtraction is intentionally modular.
570 rewardAcc := u256.Zero().Sub(endRaw, startRaw)
571
572 rewardAcc, overflow := u256.Zero().MulOverflow(rewardAcc, self.deposit.Liquidity())
573 if overflow {
574 panic(errors.New(errOverflow))
575 }
576
577 rewardAcc = u256.MulDiv(rewardAcc, u256.NewUintFromInt64(rewardPerSecond), q128)
578 self.rewards[i] = gnsmath.SafeAddInt64(self.rewards[i], gnsmath.SafeConvertToInt64(rewardAcc))
579
580 startTime = warmup.NextWarmupTime
581 startTick = endTick
582 startRaw = endRaw
583 }
584
585 return nil
586}
587
588// rewardPerWarmupX128 calculates the reward for each warmup using a Q128-scaled
589// per-second rate. Used by the external incentive path; the per-second rate is
590// stored as `(rewardAmount << 128) / duration` in ExternalIncentive, so an
591// extra `>> 128` is needed after the standard `MulDiv(rewardAcc, rps, q128)`
592// to materialize the integer result.
593func (self *RewardState) rewardPerWarmupX128(startTime, endTime int64, rewardPerSecondX128 *u256.Uint) error {
594 if startTime == endTime {
595 return nil
596 }
597
598 startTick := self.pool.CurrentTick(startTime)
599 startRaw := self.pool.CalculateRawRewardForPosition(startTime, startTick, self.deposit.Deposit)
600
601 for i, warmup := range self.deposit.Warmups() {
602 if startTime >= warmup.NextWarmupTime {
603 continue
604 }
605
606 if endTime < warmup.NextWarmupTime {
607 endTick := self.pool.CurrentTick(endTime)
608 endRaw := self.pool.CalculateRawRewardForPosition(endTime, endTick, self.deposit.Deposit)
609 // Modular by design: boundary ticks created at different times give a
610 // wrapped base, so the borrow is expected and the wrapped difference
611 // is the true accumulation (Uniswap V3 subtracts unchecked too).
612 rewardAcc := u256.Zero().Sub(endRaw, startRaw)
613
614 rewardAcc, overflow := u256.Zero().MulOverflow(rewardAcc, self.deposit.Liquidity())
615 if overflow {
616 panic(errors.New(errOverflow))
617 }
618
619 rewardAcc = u256.MulDiv(rewardAcc, rewardPerSecondX128, q128)
620 rewardAcc = u256.Zero().Rsh(rewardAcc, 128)
621 self.rewards[i] = gnsmath.SafeAddInt64(self.rewards[i], gnsmath.SafeConvertToInt64(rewardAcc))
622
623 break
624 }
625
626 endTick := self.pool.CurrentTick(warmup.NextWarmupTime)
627 endRaw := self.pool.CalculateRawRewardForPosition(warmup.NextWarmupTime, endTick, self.deposit.Deposit)
628 // See the note above: the subtraction is intentionally modular.
629 rewardAcc := u256.Zero().Sub(endRaw, startRaw)
630
631 rewardAcc, overflow := u256.Zero().MulOverflow(rewardAcc, self.deposit.Liquidity())
632 if overflow {
633 panic(errors.New(errOverflow))
634 }
635
636 rewardAcc = u256.MulDiv(rewardAcc, rewardPerSecondX128, q128)
637 rewardAcc = u256.Zero().Rsh(rewardAcc, 128)
638 self.rewards[i] = gnsmath.SafeAddInt64(self.rewards[i], gnsmath.SafeConvertToInt64(rewardAcc))
639
640 startTime = warmup.NextWarmupTime
641 startTick = endTick
642 startRaw = endRaw
643 }
644
645 return nil
646}
647
648// modifyDeposit updates the pool's staked liquidity and returns the new staked liquidity.
649// updates when there is a change in the staked liquidity(tick cross, stake, unstake)
650func (self *PoolResolver) modifyDeposit(delta *i256.Int, currentTime int64, nextTick int32) *u256.Uint {
651 // update staker side pool info
652 lastStakedLiquidity := self.CurrentStakedLiquidity(currentTime)
653 deltaApplied := gnsmath.LiquidityMathAddDelta(lastStakedLiquidity, delta)
654 result := self.updateGlobalRewardRatioAccumulation(currentTime, lastStakedLiquidity)
655
656 // historical tick does NOT actually reflect the tick at the timestamp, but it provides correct ordering for the staked positions
657 // because TickCrossHook is assured to be called for the staked-initialized ticks
658 if self.isChangedTick(currentTime, nextTick) {
659 self.Pool.SetHistoricalTickAt(currentTime, nextTick)
660 }
661
662 switch deltaApplied.Sign() {
663 case -1:
664 panic("stakedLiquidity is less than 0, should not happen")
665 case 0:
666 if lastStakedLiquidity.Sign() == 1 {
667 // StakedLiquidity moved from positive to zero, start unclaimable period
668 self.startUnclaimablePeriod(currentTime)
669 self.IncentivesResolver().startUnclaimablePeriod(currentTime)
670 }
671 case 1:
672 if lastStakedLiquidity.Sign() == 0 {
673 // StakedLiquidity moved from zero to positive, end unclaimable period
674 self.endUnclaimablePeriod(currentTime)
675 self.IncentivesResolver().endUnclaimablePeriod(currentTime)
676 }
677 }
678
679 // Only append a staked-liquidity entry when the value actually changes (e.g. a tick cross whose
680 // net delta is zero leaves it unchanged). Unlike the global reward ratio accumulation, this tree
681 // carries no time-checkpoint semantics: it is read purely as a point-in-time value via
682 // CurrentStakedLiquidity (latest entry <= t), so omitting a duplicate-valued entry preserves
683 // behavior while keeping this append-only tree from growing on no-op updates.
684 if !lastStakedLiquidity.Eq(deltaApplied) {
685 self.Pool.SetStakedLiquidityAt(currentTime, deltaApplied)
686 }
687
688 return result
689}
690
691// startUnclaimablePeriod starts the unclaimable period.
692func (self *PoolResolver) startUnclaimablePeriod(currentTime int64) {
693 if self.LastUnclaimableTime() == 0 {
694 // We set only if it's the first time entering(0 indicates not set yet)
695 self.SetLastUnclaimableTime(currentTime)
696 }
697}
698
699// endUnclaimablePeriod ends the unclaimable period.
700// Accumulates to unclaimableAcc and resets lastUnclaimableTime to 0.
701func (self *PoolResolver) endUnclaimablePeriod(currentTime int64) {
702 if self.LastUnclaimableTime() == 0 {
703 // lastUnclaimableTime = 0 means tracking hasn't started yet
704 // This is normal during initial pool creation or when called from cacheReward
705 // during tier assignment with zero liquidity
706 return
707 }
708
709 self.updateUnclaimableAccumulateRewards(currentTime)
710 self.SetLastUnclaimableTime(0)
711}
712
713// updateUnclaimableAccumulateRewards ends the unclaimable period.
714// Accumulates to unclaimableAcc and resets lastUnclaimableTime to 0.
715func (self *PoolResolver) updateUnclaimableAccumulateRewards(currentTime int64) {
716 if self.LastUnclaimableTime() >= currentTime {
717 return
718 }
719
720 unclaimableDuration := gnsmath.SafeSubInt64(currentTime, self.LastUnclaimableTime())
721 currentUnclaimableReward := gnsmath.SafeMulInt64(unclaimableDuration, self.CurrentReward(self.LastUnclaimableTime()))
722 self.SetUnclaimableAcc(gnsmath.SafeAddInt64(self.UnclaimableAcc(), currentUnclaimableReward))
723}
724
725// processUnclaimableReward processes the unclaimable reward and returns the accumulated reward.
726// It resets unclaimableAcc to 0 and properly manages lastUnclaimableTime based on pool state.
727func (self *PoolResolver) processUnclaimableReward(endTime int64) int64 {
728 // Check current pool liquidity state
729 isZeroStakedLiquidity := self.CurrentStakedLiquidity(endTime).IsZero()
730
731 if self.LastUnclaimableTime() > 0 {
732 // We have an ongoing unclaimable period tracking
733 self.updateUnclaimableAccumulateRewards(endTime)
734
735 if isZeroStakedLiquidity {
736 // Still unclaimable - accumulate rewards up to endTime
737 // Update tracking time for continuing unclaimable period
738 self.SetLastUnclaimableTime(endTime)
739 } else {
740 // Was unclaimable but now has liquidity - properly end the period
741 self.SetLastUnclaimableTime(0)
742 }
743 } else {
744 if isZeroStakedLiquidity {
745 // No previous tracking but currently unclaimable - this shouldn't normally happen
746 // as startUnclaimablePeriod should have been called when liquidity reached 0
747 // Start tracking from now
748 self.SetLastUnclaimableTime(endTime)
749 }
750 }
751
752 // Return and reset accumulated unclaimable rewards
753 internalUnClaimable := self.UnclaimableAcc()
754 self.SetUnclaimableAcc(0)
755 return internalUnClaimable
756}
757
758// CalculateRawRewardForPosition calculates the theoretical reward accumulator for a position without debt or warmup adjustments.
759//
760// Parameters:
761// - currentTime: Unix timestamp at which pool reward state is evaluated.
762// - currentTick: Pool tick used to determine whether the position is below, inside, or above its range.
763// - deposit: Position deposit whose liquidity and boundary ticks determine the raw reward.
764//
765// Returns:
766// - reward: Q128-scaled raw reward accumulator for the position; debt, warmup ratios, and fees are not applied.
767func (self *PoolResolver) CalculateRawRewardForPosition(currentTime int64, currentTick int32, deposit *sr.Deposit) *u256.Uint {
768 var rewardAcc *u256.Uint
769
770 globalAcc := self.calculateGlobalRewardRatioAccumulation(currentTime, self.CurrentStakedLiquidity(currentTime))
771
772 lowerAcc := self.outsideAccumulationAt(deposit.TickLower(), currentTime)
773 upperAcc := self.outsideAccumulationAt(deposit.TickUpper(), currentTime)
774 if currentTick < deposit.TickLower() {
775 rewardAcc = u256.Zero().Sub(lowerAcc, upperAcc)
776 } else if currentTick >= deposit.TickUpper() {
777 rewardAcc = u256.Zero().Sub(upperAcc, lowerAcc)
778 } else {
779 rewardAcc = u256.Zero().Sub(globalAcc, lowerAcc)
780 rewardAcc = rewardAcc.Sub(rewardAcc, upperAcc)
781 }
782
783 return rewardAcc
784}