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