Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}