reward_calculation_pool.gno
22.04 Kb · 647 lines
1package staker
2
3import (
4 "errors"
5 "time"
6
7 "gno.land/p/gnoswap/gnsmath"
8 bptree "gno.land/p/nt/bptree/v0"
9 ufmt "gno.land/p/nt/ufmt/v0"
10
11 i256 "gno.land/p/gnoswap/int256"
12 u256 "gno.land/p/gnoswap/uint256"
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 *bptree.BPTree // string poolPath -> pool
21}
22
23func NewPools() *Pools {
24 return &Pools{
25 tree: sr.NewBPTreeN(16),
26 }
27}
28
29// Get returns the pool for the given poolPath
30func (self *Pools) Get(poolPath string) (*sr.Pool, bool) {
31 v := self.tree.Get(poolPath)
32 if v == nil {
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 string) {
82 acc = "0"
83
84 self.GlobalRewardRatioAccumulation().ReverseIterate(0, currentTime, func(key int64, value any) bool {
85 time = key
86
87 valueStr, ok := value.(string)
88 if !ok {
89 panic(ufmt.Sprintf("failed to cast value to string: %T", value))
90 }
91
92 acc = valueStr
93
94 return true
95 })
96
97 return time, acc
98}
99
100// Get the latest tick in [0, currentTime] range.
101// Returns the tick.
102func (self *PoolResolver) CurrentTick(currentTime int64) (tick int32) {
103 self.HistoricalTick().ReverseIterate(0, currentTime, func(key int64, value any) bool {
104 res, ok := value.(int32)
105 if !ok {
106 panic(ufmt.Sprintf("failed to cast value to int32: %T", value))
107 }
108 tick = res
109 return true
110 })
111 return tick
112}
113
114func (self *PoolResolver) CurrentStakedLiquidity(currentTime int64) (liquidity *u256.Uint) {
115 liquidity = u256.Zero()
116
117 self.StakedLiquidity().ReverseIterate(0, currentTime, func(key int64, value any) bool {
118 res, ok := value.(*u256.Uint)
119 if !ok {
120 panic(ufmt.Sprintf("failed to cast value to *u256.Uint: %T", value))
121 }
122 liquidity = res
123 return true
124 })
125 return liquidity
126}
127
128// GetOrNewTick returns the existing tick or a new zero-valued tick.
129//
130// Substituting a zero-valued tick on a read is safe because ticks are pruned
131// only when their staked gross liquidity reaches zero, in the same call that
132// removes the last deposit referencing them.
133func (self *PoolResolver) GetOrNewTick(tickId int32) *sr.Tick {
134 tick := self.Ticks().Get(tickId)
135 if tick == nil {
136 return sr.NewTick(tickId)
137 }
138 return tick
139}
140
141// IsExternallyIncentivizedPool returns true if the pool has any active external incentives.
142func (self *PoolResolver) IsExternallyIncentivizedPool() bool {
143 currentTime := time.Now().Unix()
144 hasIncentive := false
145 self.Incentives().IncentiveTrees().Iterate("", "", func(key string, value any) bool {
146 incentive, ok := value.(*sr.ExternalIncentive)
147 if !ok {
148 panic("failed to cast value to *ExternalIncentive")
149 }
150
151 resolver := NewExternalIncentiveResolver(incentive)
152 if !resolver.IsEnded(currentTime) {
153 hasIncentive = true
154 return true
155 }
156
157 return false
158 })
159
160 return hasIncentive
161}
162
163// Get the latest reward in [0, currentTime] range.
164// Returns the reward.
165func (self *PoolResolver) CurrentReward(currentTime int64) (reward int64) {
166 self.RewardCache().ReverseIterate(0, currentTime, func(key int64, value any) bool {
167 res, ok := value.(int64)
168 if !ok {
169 panic(ufmt.Sprintf("failed to cast value to int64: %T", value))
170 }
171 reward = res
172 return true
173 })
174 return reward
175}
176
177func (self *PoolResolver) isChangedTick(currentTime int64, currentTick int32) bool {
178 if self.HistoricalTick().Size() == 0 {
179 return true
180 }
181
182 previousTick := self.CurrentTick(currentTime)
183
184 return previousTick != currentTick
185}
186
187// cacheReward sets the current reward for the pool
188// If the pool is in unclaimable period, it will end the unclaimable period, updates the reward, and start the unclaimable period again.
189//
190// Important behavior for initial tier assignment:
191// - When a pool first receives a tier, oldTierReward=0 and currentTierReward>0
192// - If the pool has zero liquidity at this point, startUnclaimablePeriod() is called
193// - This ensures unclaimable period tracking begins from the moment rewards start emitting
194func (self *PoolResolver) cacheReward(currentTime int64, currentTierReward int64) {
195 oldTierReward := self.CurrentReward(currentTime)
196 if oldTierReward == currentTierReward {
197 return
198 }
199
200 isInUnclaimable := self.CurrentStakedLiquidity(currentTime).IsZero()
201 if isInUnclaimable {
202 // End any existing unclaimable period
203 // Note: If lastUnclaimableTime is 0 (not yet tracking), this is a no-op
204 self.endUnclaimablePeriod(currentTime)
205 }
206
207 self.Pool.SetRewardCacheAt(currentTime, currentTierReward)
208
209 if isInUnclaimable {
210 // Start/restart unclaimable period tracking
211 // This handles initial tier assignment when lastUnclaimableTime is 0
212 self.startUnclaimablePeriod(currentTime)
213 }
214}
215
216func (self *PoolResolver) calculateGlobalRewardRatioAccumulation(currentTime int64, currentStakedLiquidity *u256.Uint) *u256.Uint {
217 oldAccTime, oldAccStr := self.CurrentGlobalRewardRatioAccumulation(currentTime)
218 timeDiff := gnsmath.SafeSubInt64(currentTime, oldAccTime)
219 if timeDiff == 0 {
220 return u256.MustFromDecimal(oldAccStr)
221 }
222 if timeDiff < 0 {
223 panic("time cannot go backwards")
224 }
225
226 if currentStakedLiquidity.IsZero() {
227 return u256.MustFromDecimal(oldAccStr)
228 }
229
230 oldAcc := u256.MustFromDecimal(oldAccStr)
231 acc := u256.MulDiv(
232 u256.NewUintFromInt64(timeDiff),
233 q128,
234 currentStakedLiquidity,
235 )
236 return u256.Zero().Add(oldAcc, acc)
237}
238
239// globalRewardRatioAccumulationAt returns the global reward ratio accumulation *at* currentTime.
240//
241// CurrentGlobalRewardRatioAccumulation returns the latest stored checkpoint (<= currentTime), which is
242// only equal to the accumulation at currentTime when a checkpoint was written at that very timestamp.
243// Checkpoints are written exclusively by modifyDeposit (staked liquidity changes), so on any other path
244// the stored value lags by (currentTime - lastCheckpointTime) * q128 / stakedLiquidity.
245//
246// Reward calculation never has this problem because it derives the accumulation on demand
247// (CalculateRawRewardForPosition). Event emission must do the same, otherwise off-chain indexers that
248// treat the emitted accumulator as authoritative at the event timestamp silently drop that interval.
249func (self *PoolResolver) globalRewardRatioAccumulationAt(currentTime int64) (*u256.Uint, *u256.Uint) {
250 stakedLiquidity := self.CurrentStakedLiquidity(currentTime)
251 accumulation := self.calculateGlobalRewardRatioAccumulation(currentTime, stakedLiquidity)
252
253 return accumulation, stakedLiquidity
254}
255
256// updateGlobalRewardRatioAccumulation updates the global reward ratio accumulation and returns the new accumulation.
257func (self *PoolResolver) updateGlobalRewardRatioAccumulation(currentTime int64, currentStakedLiquidity *u256.Uint) *u256.Uint {
258 newAcc := self.calculateGlobalRewardRatioAccumulation(currentTime, currentStakedLiquidity)
259
260 // Persist as string to reduce stored object complexity.
261 self.Pool.SetGlobalRewardRatioAccumulationAt(currentTime, newAcc.ToString())
262 return newAcc
263}
264
265// RewardStateOf initializes a new RewardState for the given deposit.
266func (self *PoolResolver) RewardStateOf(deposit *sr.Deposit) *RewardState {
267 warmups := len(deposit.Warmups())
268 result := &RewardState{
269 pool: self,
270 deposit: NewDepositResolver(deposit),
271 rewards: make([]int64, warmups),
272 penalties: make([]int64, warmups),
273 }
274
275 return result
276}
277
278// reset clears cached rewards/penalties so a RewardState can be reused without re-allocating.
279func (self *RewardState) reset() {
280 for i := range self.rewards {
281 self.rewards[i] = 0
282 self.penalties[i] = 0
283 }
284}
285
286// NewPool creates a new pool with the given poolPath and currentHeight.
287func NewPoolResolver(pool *sr.Pool) *PoolResolver {
288 return &PoolResolver{
289 Pool: pool,
290 }
291}
292
293// RewardState is a struct for storing the intermediate state for reward calculation.
294type RewardState struct {
295 pool *PoolResolver
296 deposit *DepositResolver
297
298 // accumulated rewards for each warmup
299 rewards []int64
300 penalties []int64
301}
302
303// calculateInternalReward computes the position's per-warmup rewards and penalties from a pre-resolved
304// per-second reward-rate schedule (see PoolResolver.resolveInternalRewardSegments).
305//
306// It is pure: it neither queries pool tier/emission state nor writes any state, so the read-only view
307// path and the collect path use it identically. Each segment [start, end) is applied at its constant
308// per-second rate; rewardPerWarmup is a no-op for empty segments (start == end).
309func (self *RewardState) calculateInternalReward(segments []internalRewardSegment) ([]int64, []int64) {
310 for _, seg := range segments {
311 if err := self.rewardPerWarmup(seg.start, seg.end, seg.rewardPerSecond); err != nil {
312 panic(err)
313 }
314 }
315
316 self.applyWarmup()
317
318 return self.rewards, self.penalties
319}
320
321// updateExternalReward updates the external reward for the deposit.
322// It updates the last collect time for the external reward for the given incentive ID.
323// It returns an error if the current time is less than the last collect time for the external reward for the given incentive ID.
324func (self *RewardState) updateExternalReward(startTime, endTime int64, incentive *sr.ExternalIncentive) error {
325 lastCollectTime := self.deposit.ExternalRewardLastCollectTime(incentive.IncentiveId())
326 if startTime < lastCollectTime {
327 // This must not happen, but adding some guards just in case.
328 startTime = lastCollectTime
329 }
330
331 ictvStart := incentive.StartTimestamp()
332 if endTime < ictvStart {
333 return nil // Not started yet
334 }
335
336 if startTime < ictvStart {
337 startTime = ictvStart
338 }
339
340 ictvEnd := incentive.EndTimestamp()
341 if endTime > ictvEnd {
342 endTime = ictvEnd
343 }
344
345 if startTime > ictvEnd {
346 return nil // Already ended
347 }
348
349 return self.rewardPerWarmupX128(startTime, endTime, incentive.RewardPerSecondX128())
350}
351
352// calculateCollectableExternalReward calculates the calculated external reward for the deposit.
353// It calls updateExternalReward for the incentive period, applies warmup and returns the rewards and penalties.
354// used for reward calculation for a calculatable incentive
355func (self *RewardState) calculateCollectableExternalReward(startTime, endTime int64, incentive *sr.ExternalIncentive) int64 {
356 err := self.updateExternalReward(startTime, endTime, incentive)
357 if err != nil {
358 panic(err)
359 }
360
361 currentReward := u256.Zero()
362
363 for i := range self.rewards {
364 currentReward = currentReward.Add(currentReward, u256.NewUintFromInt64(self.rewards[i]))
365 }
366
367 return gnsmath.SafeConvertToInt64(currentReward)
368}
369
370// calculateExternalReward calculates the external reward for the deposit.
371// It calls rewardPerWarmup for startTime to endTime(clamped to the incentive period), applies warmup and returns the rewards and penalties.
372func (self *RewardState) calculateExternalReward(startTime, endTime int64, incentive *sr.ExternalIncentive) ([]int64, []int64) {
373 err := self.updateExternalReward(startTime, endTime, incentive)
374 if err != nil {
375 panic(err)
376 }
377
378 // apply warmup to collect rewards
379 self.applyWarmup()
380
381 return self.rewards, self.penalties
382}
383
384// applyWarmup applies the warmup to the rewards and calculate penalties.
385func (self *RewardState) applyWarmup() {
386 for i, warmup := range self.deposit.Warmups() {
387 warmupReward := self.rewards[i]
388
389 // calculate warmup reward applying warmup ratio
390 self.rewards[i] = gnsmath.SafeMulDivInt64(warmupReward, int64(warmup.WarmupRatio), 100)
391
392 // warmup penalty is the difference between the warmup reward and the warmup reward applying warmup ratio
393 self.penalties[i] = gnsmath.SafeSubInt64(warmupReward, self.rewards[i])
394 }
395}
396
397// rewardPerWarmup calculates the reward for each warmup, adds to the RewardState's rewards array.
398// Used by the internal reward path where rewardPerSecond is an int64 emission rate.
399func (self *RewardState) rewardPerWarmup(startTime, endTime int64, rewardPerSecond int64) error {
400 // Return early if startTime equals endTime to avoid unnecessary computation
401 if startTime == endTime {
402 return nil
403 }
404
405 startTick := self.pool.CurrentTick(startTime)
406 startRaw := self.pool.CalculateRawRewardForPosition(startTime, startTick, self.deposit.Deposit)
407
408 for i, warmup := range self.deposit.Warmups() {
409 if startTime >= warmup.NextWarmupTime {
410 // passed the warmup
411 continue
412 }
413
414 if endTime < warmup.NextWarmupTime {
415 endTick := self.pool.CurrentTick(endTime)
416 endRaw := self.pool.CalculateRawRewardForPosition(endTime, endTick, self.deposit.Deposit)
417 rewardAcc, overflow := u256.Zero().SubOverflow(endRaw, startRaw)
418 if overflow {
419 panic(errors.New(errOverflow))
420 }
421
422 rewardAcc, overflow = u256.Zero().MulOverflow(rewardAcc, self.deposit.Liquidity())
423 if overflow {
424 panic(errors.New(errOverflow))
425 }
426
427 rewardAcc = u256.MulDiv(rewardAcc, u256.NewUintFromInt64(rewardPerSecond), q128)
428 self.rewards[i] = gnsmath.SafeAddInt64(self.rewards[i], gnsmath.SafeConvertToInt64(rewardAcc))
429
430 break
431 }
432
433 endTick := self.pool.CurrentTick(warmup.NextWarmupTime)
434 endRaw := self.pool.CalculateRawRewardForPosition(warmup.NextWarmupTime, endTick, self.deposit.Deposit)
435 rewardAcc, overflow := u256.Zero().SubOverflow(endRaw, startRaw)
436 if overflow {
437 panic(errors.New(errOverflow))
438 }
439
440 rewardAcc, overflow = u256.Zero().MulOverflow(rewardAcc, self.deposit.Liquidity())
441 if overflow {
442 panic(errors.New(errOverflow))
443 }
444
445 rewardAcc = u256.MulDiv(rewardAcc, u256.NewUintFromInt64(rewardPerSecond), q128)
446 self.rewards[i] = gnsmath.SafeAddInt64(self.rewards[i], gnsmath.SafeConvertToInt64(rewardAcc))
447
448 startTime = warmup.NextWarmupTime
449 startTick = endTick
450 startRaw = endRaw
451 }
452
453 return nil
454}
455
456// rewardPerWarmupX128 calculates the reward for each warmup using a Q128-scaled
457// per-second rate. Used by the external incentive path; the per-second rate is
458// stored as `(rewardAmount << 128) / duration` in ExternalIncentive, so an
459// extra `>> 128` is needed after the standard `MulDiv(rewardAcc, rps, q128)`
460// to materialize the integer result.
461func (self *RewardState) rewardPerWarmupX128(startTime, endTime int64, rewardPerSecondX128 *u256.Uint) error {
462 if startTime == endTime {
463 return nil
464 }
465
466 startTick := self.pool.CurrentTick(startTime)
467 startRaw := self.pool.CalculateRawRewardForPosition(startTime, startTick, self.deposit.Deposit)
468
469 for i, warmup := range self.deposit.Warmups() {
470 if startTime >= warmup.NextWarmupTime {
471 continue
472 }
473
474 if endTime < warmup.NextWarmupTime {
475 endTick := self.pool.CurrentTick(endTime)
476 endRaw := self.pool.CalculateRawRewardForPosition(endTime, endTick, self.deposit.Deposit)
477 rewardAcc, overflow := u256.Zero().SubOverflow(endRaw, startRaw)
478 if overflow {
479 panic(errors.New(errOverflow))
480 }
481
482 rewardAcc, overflow = u256.Zero().MulOverflow(rewardAcc, self.deposit.Liquidity())
483 if overflow {
484 panic(errors.New(errOverflow))
485 }
486
487 rewardAcc = u256.MulDiv(rewardAcc, rewardPerSecondX128, q128)
488 rewardAcc = u256.Zero().Rsh(rewardAcc, 128)
489 self.rewards[i] = gnsmath.SafeAddInt64(self.rewards[i], gnsmath.SafeConvertToInt64(rewardAcc))
490
491 break
492 }
493
494 endTick := self.pool.CurrentTick(warmup.NextWarmupTime)
495 endRaw := self.pool.CalculateRawRewardForPosition(warmup.NextWarmupTime, endTick, self.deposit.Deposit)
496 rewardAcc, overflow := u256.Zero().SubOverflow(endRaw, startRaw)
497 if overflow {
498 panic(errors.New(errOverflow))
499 }
500
501 rewardAcc, overflow = u256.Zero().MulOverflow(rewardAcc, self.deposit.Liquidity())
502 if overflow {
503 panic(errors.New(errOverflow))
504 }
505
506 rewardAcc = u256.MulDiv(rewardAcc, rewardPerSecondX128, q128)
507 rewardAcc = u256.Zero().Rsh(rewardAcc, 128)
508 self.rewards[i] = gnsmath.SafeAddInt64(self.rewards[i], gnsmath.SafeConvertToInt64(rewardAcc))
509
510 startTime = warmup.NextWarmupTime
511 startTick = endTick
512 startRaw = endRaw
513 }
514
515 return nil
516}
517
518// modifyDeposit updates the pool's staked liquidity and returns the new staked liquidity.
519// updates when there is a change in the staked liquidity(tick cross, stake, unstake)
520func (self *PoolResolver) modifyDeposit(delta *i256.Int, currentTime int64, nextTick int32) *u256.Uint {
521 // update staker side pool info
522 lastStakedLiquidity := self.CurrentStakedLiquidity(currentTime)
523 deltaApplied := gnsmath.LiquidityMathAddDelta(lastStakedLiquidity, delta)
524 result := self.updateGlobalRewardRatioAccumulation(currentTime, lastStakedLiquidity)
525
526 // historical tick does NOT actually reflect the tick at the timestamp, but it provides correct ordering for the staked positions
527 // because TickCrossHook is assured to be called for the staked-initialized ticks
528 if self.isChangedTick(currentTime, nextTick) {
529 self.Pool.SetHistoricalTickAt(currentTime, nextTick)
530 }
531
532 switch deltaApplied.Sign() {
533 case -1:
534 panic("stakedLiquidity is less than 0, should not happen")
535 case 0:
536 if lastStakedLiquidity.Sign() == 1 {
537 // StakedLiquidity moved from positive to zero, start unclaimable period
538 self.startUnclaimablePeriod(currentTime)
539 self.IncentivesResolver().startUnclaimablePeriod(currentTime)
540 }
541 case 1:
542 if lastStakedLiquidity.Sign() == 0 {
543 // StakedLiquidity moved from zero to positive, end unclaimable period
544 self.endUnclaimablePeriod(currentTime)
545 self.IncentivesResolver().endUnclaimablePeriod(currentTime)
546 }
547 }
548
549 // Only append a staked-liquidity entry when the value actually changes (e.g. a tick cross whose
550 // net delta is zero leaves it unchanged). Unlike the global reward ratio accumulation, this tree
551 // carries no time-checkpoint semantics: it is read purely as a point-in-time value via
552 // CurrentStakedLiquidity (latest entry <= t), so omitting a duplicate-valued entry preserves
553 // behavior while keeping this append-only tree from growing on no-op updates.
554 if !lastStakedLiquidity.Eq(deltaApplied) {
555 self.Pool.SetStakedLiquidityAt(currentTime, deltaApplied)
556 }
557
558 return result
559}
560
561// startUnclaimablePeriod starts the unclaimable period.
562func (self *PoolResolver) startUnclaimablePeriod(currentTime int64) {
563 if self.LastUnclaimableTime() == 0 {
564 // We set only if it's the first time entering(0 indicates not set yet)
565 self.SetLastUnclaimableTime(currentTime)
566 }
567}
568
569// endUnclaimablePeriod ends the unclaimable period.
570// Accumulates to unclaimableAcc and resets lastUnclaimableTime to 0.
571func (self *PoolResolver) endUnclaimablePeriod(currentTime int64) {
572 if self.LastUnclaimableTime() == 0 {
573 // lastUnclaimableTime = 0 means tracking hasn't started yet
574 // This is normal during initial pool creation or when called from cacheReward
575 // during tier assignment with zero liquidity
576 return
577 }
578
579 self.updateUnclaimableAccumulateRewards(currentTime)
580 self.SetLastUnclaimableTime(0)
581}
582
583// updateUnclaimableAccumulateRewards ends the unclaimable period.
584// Accumulates to unclaimableAcc and resets lastUnclaimableTime to 0.
585func (self *PoolResolver) updateUnclaimableAccumulateRewards(currentTime int64) {
586 if self.LastUnclaimableTime() >= currentTime {
587 return
588 }
589
590 unclaimableDuration := gnsmath.SafeSubInt64(currentTime, self.LastUnclaimableTime())
591 currentUnclaimableReward := gnsmath.SafeMulInt64(unclaimableDuration, self.CurrentReward(self.LastUnclaimableTime()))
592 self.SetUnclaimableAcc(gnsmath.SafeAddInt64(self.UnclaimableAcc(), currentUnclaimableReward))
593}
594
595// processUnclaimableReward processes the unclaimable reward and returns the accumulated reward.
596// It resets unclaimableAcc to 0 and properly manages lastUnclaimableTime based on pool state.
597func (self *PoolResolver) processUnclaimableReward(endTime int64) int64 {
598 // Check current pool liquidity state
599 isZeroStakedLiquidity := self.CurrentStakedLiquidity(endTime).IsZero()
600
601 if self.LastUnclaimableTime() > 0 {
602 // We have an ongoing unclaimable period tracking
603 self.updateUnclaimableAccumulateRewards(endTime)
604
605 if isZeroStakedLiquidity {
606 // Still unclaimable - accumulate rewards up to endTime
607 // Update tracking time for continuing unclaimable period
608 self.SetLastUnclaimableTime(endTime)
609 } else {
610 // Was unclaimable but now has liquidity - properly end the period
611 self.SetLastUnclaimableTime(0)
612 }
613 } else {
614 if isZeroStakedLiquidity {
615 // No previous tracking but currently unclaimable - this shouldn't normally happen
616 // as startUnclaimablePeriod should have been called when liquidity reached 0
617 // Start tracking from now
618 self.SetLastUnclaimableTime(endTime)
619 }
620 }
621
622 // Return and reset accumulated unclaimable rewards
623 internalUnClaimable := self.UnclaimableAcc()
624 self.SetUnclaimableAcc(0)
625 return internalUnClaimable
626}
627
628// Calculates reward for a position *without* considering debt or warmup
629// It calculates the theoretical total reward for the position if it has been staked since the pool creation
630func (self *PoolResolver) CalculateRawRewardForPosition(currentTime int64, currentTick int32, deposit *sr.Deposit) *u256.Uint {
631 var rewardAcc *u256.Uint
632
633 globalAcc := self.calculateGlobalRewardRatioAccumulation(currentTime, self.CurrentStakedLiquidity(currentTime))
634
635 lowerAcc := NewTickResolver(self.GetOrNewTick(deposit.TickLower())).CurrentOutsideAccumulation(currentTime)
636 upperAcc := NewTickResolver(self.GetOrNewTick(deposit.TickUpper())).CurrentOutsideAccumulation(currentTime)
637 if currentTick < deposit.TickLower() {
638 rewardAcc = u256.Zero().Sub(lowerAcc, upperAcc)
639 } else if currentTick >= deposit.TickUpper() {
640 rewardAcc = u256.Zero().Sub(upperAcc, lowerAcc)
641 } else {
642 rewardAcc = u256.Zero().Sub(globalAcc, lowerAcc)
643 rewardAcc = rewardAcc.Sub(rewardAcc, upperAcc)
644 }
645
646 return rewardAcc
647}