Kind Realm (renderable)
Name v1
Namespace gnoswap / staker
Exported functions 14
Module gno.land/r/gnoswap/staker/v1
gno 0.9 calculate_pool_position_reward.gno gno
⧉
1 package staker 2 3 import ( 4 "time" 5 6 "gno.land/p/gnoswap/gnsmath/v1" 7 ufmt "gno.land/p/nt/ufmt/v0" 8 NewDepositResolver (deposit *gno.land/r/gnoswap/staker.Deposit) *gno.land/r/gnoswap/staker/v1.DepositResolver
NewDeposits () *gno.land/r/gnoswap/staker/v1.Deposits
NewExternalIncentiveResolver (externalIncentive *gno.land/r/gnoswap/staker.ExternalIncentive) *gno.land/r/gnoswap/staker/v1.ExternalIncentiveResolver
NewExternalIncentives () *gno.land/r/gnoswap/staker/v1.ExternalIncentives
NewIncentivesResolver (incentives *gno.land/r/gnoswap/staker.Incentives) *gno.land/r/gnoswap/staker/v1.IncentivesResolver
NewPoolResolver (pool *gno.land/r/gnoswap/staker.Pool) *gno.land/r/gnoswap/staker/v1.PoolResolver
NewPools () *gno.land/r/gnoswap/staker/v1.Pools
NewPoolTier (pools *gno.land/r/gnoswap/staker/v1.Pools, currentTime int64, initialPoolPath string, getEmission func() (int64, .uverse.error), getHalvingBlocksInRange func(int64, int64) ([]int64, []int64, .uverse.error)) *gno.land/r/gnoswap/staker/v1.PoolTier
NewPoolTierBy (membership *gno.land/p/nt/bptree/v0.BPTree, tierRatio struct{Tier1 uint64; Tier2 uint64; Tier3 uint64}, counts [4]uint64, lastRewardCacheTimestamp int64, currentEmission int64, getEmission func() (int64, .uverse.error), getHalvingBlocksInRange func(int64, int64) ([]int64, []int64, .uverse.error)) *gno.land/r/gnoswap/staker/v1.PoolTier
NewStakerV1 (stakerStore interface {AddAllowedToken func(int, .uverse.realm, string) .uverse.error; AddDeniedRewardToken func(int, .uverse.realm, string) .uverse.error; GetAllowedTokens func() []string; GetCurrentSwapBatch func() *gno.land/r/gnoswap/staker.SwapBatchProcessor; GetDeniedRewardTokens func() []string; GetDepositGnsAmount func() int64; GetDeposits func() *gno.land/p/nt/bptree/v0.BPTree; GetExternalIncentives func() *gno.land/p/nt/bptree/v0.BPTree; GetIncentiveCounter func() *gno.land/r/gnoswap/staker.Counter; GetMinimumRewardAmount func() int64; GetPendingProtocolFee func(string) int64; GetPendingProtocolFees func() map[string]int64; GetPoolTierCounts func() [4]uint64; GetPoolTierCurrentEmission func() int64; GetPoolTierGetEmission func() func() (int64, .uverse.error); GetPoolTierGetHalvingBlocksInRange func() func(int64, int64) ([]int64, []int64, .uverse.error); GetPoolTierLastRewardCacheTimestamp func() int64; GetPoolTierMemberships func() *gno.land/p/nt/bptree/v0.BPTree; GetPoolTierRatio func() gno.land/r/gnoswap/staker.TierRatio; GetPools func() *gno.land/p/nt/bptree/v0.BPTree; GetTokenSpecificMinimumRewards func() map[string]int64; GetTotalEmissionSent func() int64; GetUncollectedIncentiveCounts func() *gno.land/p/nt/bptree/v0.BPTree; GetUnstakedPositions func() *gno.land/p/nt/bptree/v0.BPTree; GetUnstakingFee func() uint64; GetWarmupTemplate func() []gno.land/r/gnoswap/staker.Warmup; HasAllowedTokensStoreKey func() bool; HasCurrentSwapBatchStoreKey func() bool; HasDeniedRewardTokensStoreKey func() bool; HasDepositGnsAmountStoreKey func() bool; HasDepositsStoreKey func() bool; HasExternalIncentivesStoreKey func() bool; HasIncentiveCounterStoreKey func() bool; HasMinimumRewardAmountStoreKey func() bool; HasPendingProtocolFeesStoreKey func() bool; HasPoolTierCountsStoreKey func() bool; HasPoolTierCurrentEmissionStoreKey func() bool; HasPoolTierGetEmissionStoreKey func() bool; HasPoolTierGetHalvingBlocksInRangeStoreKey func() bool; HasPoolTierLastRewardCacheTimestampStoreKey func() bool; HasPoolTierMembershipsStoreKey func() bool; HasPoolTierRatioStoreKey func() bool; HasPoolsStoreKey func() bool; HasTokenSpecificMinimumRewardsStoreKey func() bool; HasTotalEmissionSentStoreKey func() bool; HasUncollectedIncentiveCountsStoreKey func() bool; HasUnstakedPositionsStoreKey func() bool; HasUnstakingFeeStoreKey func() bool; HasWarmupTemplateStoreKey func() bool; NextIncentiveID func(.uverse.address, int64) string; RemoveAllowedToken func(int, .uverse.realm, string) .uverse.error; RemoveDeniedRewardToken func(int, .uverse.realm, string) .uverse.error; RemovePendingProtocolFee func(int, .uverse.realm, string) .uverse.error; RemoveTokenSpecificMinimumRewardItem func(int, .uverse.realm, string) .uverse.error; SetAllowedTokens func(int, .uverse.realm, []string) .uverse.error; SetCurrentSwapBatch func(int, .uverse.realm, *gno.land/r/gnoswap/staker.SwapBatchProcessor) .uverse.error; SetDepositGnsAmount func(int, .uverse.realm, int64) .uverse.error; SetDeposits func(int, .uverse.realm, *gno.land/p/nt/bptree/v0.BPTree) .uverse.error; SetExternalIncentives func(int, .uverse.realm, *gno.land/p/nt/bptree/v0.BPTree) .uverse.error; SetIncentiveCounter func(int, .uverse.realm, *gno.land/r/gnoswap/staker.Counter) .uverse.error; SetMinimumRewardAmount func(int, .uverse.realm, int64) .uverse.error; SetPendingProtocolFee func(int, .uverse.realm, string, int64) .uverse.error; SetPendingProtocolFees func(int, .uverse.realm, map[string]int64) .uverse.error; SetPoolTierCounts func(int, .uverse.realm, [4]uint64) .uverse.error; SetPoolTierCurrentEmission func(int, .uverse.realm, int64) .uverse.error; SetPoolTierGetEmission func(int, .uverse.realm, func() (int64, .uverse.error)) .uverse.error; SetPoolTierGetHalvingBlocksInRange func(int, .uverse.realm, func(int64, int64) ([]int64, []int64, .uverse.error)) .uverse.error; SetPoolTierLastRewardCacheTimestamp func(int, .uverse.realm, int64) .uverse.error; SetPoolTierMemberships func(int, .uverse.realm, *gno.land/p/nt/bptree/v0.BPTree) .uverse.error; SetPoolTierRatio func(int, .uverse.realm, gno.land/r/gnoswap/staker.TierRatio) .uverse.error; SetPools func(int, .uverse.realm, *gno.land/p/nt/bptree/v0.BPTree) .uverse.error; SetTokenSpecificMinimumRewardItem func(int, .uverse.realm, string, int64) .uverse.error; SetTokenSpecificMinimumRewards func(int, .uverse.realm, map[string]int64) .uverse.error; SetTotalEmissionSent func(int, .uverse.realm, int64) .uverse.error; SetUncollectedIncentiveCounts func(int, .uverse.realm, *gno.land/p/nt/bptree/v0.BPTree) .uverse.error; SetUnstakedPositions func(int, .uverse.realm, *gno.land/p/nt/bptree/v0.BPTree) .uverse.error; SetUnstakingFee func(int, .uverse.realm, uint64) .uverse.error; SetWarmupTemplate func(int, .uverse.realm, []gno.land/r/gnoswap/staker.Warmup) .uverse.error}, poolAccessor interface {ExistsPoolPath func(string) bool; GetSlot0SqrtPriceX96 func(string) string; GetSlot0Tick func(string) int32; SetSwapEndHook func(int, .uverse.realm, func(int, .uverse.realm, string) .uverse.error); SetSwapStartHook func(int, .uverse.realm, func(int, .uverse.realm, string, int64)); SetTickCrossHook func(int, .uverse.realm, func(int, .uverse.realm, string, int32, bool, int64))}, emissionAccessor interface {GetStakerEmissionAmountPerSecond func() (int64, .uverse.error); GetStakerEmissionAmountPerSecondInRange func(int64, int64) ([]int64, []int64, .uverse.error); MintAndDistributeGns func(int, .uverse.realm) (int64, bool); SetOnDistributionPctChangeCallback func(int, .uverse.realm, func(int, .uverse.realm, int64))}, nftAccessor interface {Approve func(int, .uverse.realm, .uverse.address, gno.land/p/nt/grc721/v0.TokenID) .uverse.error; Burn func(int, .uverse.realm, gno.land/p/nt/grc721/v0.TokenID); Exists func(gno.land/p/nt/grc721/v0.TokenID) bool; Mint func(int, .uverse.realm, .uverse.address, gno.land/p/nt/grc721/v0.TokenID) gno.land/p/nt/grc721/v0.TokenID; MustOwnerOf func(gno.land/p/nt/grc721/v0.TokenID) .uverse.address; OwnerOf func(gno.land/p/nt/grc721/v0.TokenID) (.uverse.address, .uverse.error); TotalSupply func() int64; TransferFrom func(int, .uverse.realm, .uverse.address, .uverse.address, gno.land/p/nt/grc721/v0.TokenID) .uverse.error})
NewTickCrossEventInfo (tickID int32, stakedLiquidityGross *gno.land/p/gnoswap/uint256/v1.Uint, stakedLiquidityDelta *gno.land/p/gnoswap/int256/v1.Int, outsideAccumulation *gno.land/p/gnoswap/uint256/v1.Uint) *gno.land/r/gnoswap/staker/v1.tickCrossEventInfo
NewTickResolver (tick *gno.land/r/gnoswap/staker.Tick) *gno.land/r/gnoswap/staker/v1.TickResolver
NewUnstakedPositions () *gno.land/r/gnoswap/staker/v1.UnstakedPositions
TierRatioFromCounts (tier1Count uint64, tier2Count uint64, tier3Count uint64) struct{Tier1 uint64; Tier2 uint64; Tier3 uint64}
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
This realm does not provide renderable output (vm/qrender → NoRenderDeclError).
View on gnoweb ↗ vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.
9 sr "gno.land/r/gnoswap/staker"
10 )
11
12 // Reward is a struct for storing reward for a position.
13 // Internal reward is the GNS reward, external reward is the reward for other incentives.
14 // Penalties are the amount that is deducted from the reward due to the position's warmup.
15 type Reward struct {
16 Internal int64
17 InternalPenalty int64
18 External map [ string ] int64 // Incentive ID -> TokenAmount
19 ExternalPenalty map [ string ] int64 // Incentive ID -> TokenAmount
20 }
21
22 // aggregateRewards sums the per-warmup rewards/penalties into a single Reward.
23 func aggregateRewards(rewards []Reward) Reward {
24 internal := int64 ( 0 )
25 internalPenalty := int64 ( 0 )
26
27 rewardLen := len(rewards)
28 externalReward := make( map [ string ] int64 , rewardLen)
29 externalPenalty := make( map [ string ] int64 , rewardLen)
30
31 for _, reward := range rewards {
32 internal = gnsmath.SafeAddInt64(internal, reward.Internal)
33 internalPenalty = gnsmath.SafeAddInt64(internalPenalty, reward.InternalPenalty)
34
35 for incentive, amount := range reward.External {
36 externalReward[incentive] = gnsmath.SafeAddInt64(externalReward[incentive], amount)
37 }
38
39 for incentive, penalty := range reward.ExternalPenalty {
40 externalPenalty[incentive] = gnsmath.SafeAddInt64(externalPenalty[incentive], penalty)
41 }
42 }
43
44 return Reward{
45 Internal: internal,
46 InternalPenalty: internalPenalty,
47 External: externalReward,
48 ExternalPenalty: externalPenalty,
49 }
50 }
51
52 // calculatePositionRewardParam is a struct for calculating position reward
53 type calculatePositionRewardParam struct {
54 // Environmental variables
55 CurrentHeight int64
56 CurrentTime int64
57 Deposits *Deposits
58 Pools *Pools
59 PoolTier *PoolTier
60
61 // Position variables
62 PositionId uint64
63
64 // Deposit overrides the Deposits lookup for a deposit that is no longer in the tree.
65 Deposit *sr.Deposit
66 // Exit pins the pool state to the moment the position left the pool; nil while staked.
67 Exit *sr.UnstakedPosition
68 }
69
70 // deposit resolves the deposit the calculation runs against.
71 func (self *calculatePositionRewardParam) deposit() *sr.Deposit {
72 if self.Deposit != nil {
73 return self.Deposit
74 }
75
76 return self.Deposits.get(self.PositionId)
77 }
78
79 // positionRewardUpdate carries the persisted-state changes produced (but not applied) by a calculation.
80 // They are applied by the update* functions, so that only the collect path mutates state while the
81 // calculation stays pure.
82 type positionRewardUpdate struct {
83 poolPath string
84 pool *sr.Pool
85 poolExisted bool
86
87 // External incentive ids discovered as newly created since the deposit's last update; they must be
88 // added to the deposit's incentive index.
89 newExternalIncentiveIds [] string
90 // Whether the deposit's LastExternalIncentiveUpdatedAt cursor should be advanced to CurrentTime.
91 advanceExternalIncentiveCursor bool
92
93 // Incentive ids the calculation actually produced a reward for, in calculation order. The collect
94 // path walks this instead of iterating the reward map, so delivery order is deterministic.
95 externalIncentiveIds [] string
96 }
97
98 // positionRewardContext carries the per-position state that both the internal (GNS emission) and the
99 // external (incentive) reward calculations need. Resolving it once lets each path be calculated on its
100 // own without duplicating the deposit/pool/reward-state setup.
101 type positionRewardContext struct {
102 deposit *sr.Deposit
103 depositResolver *DepositResolver
104 poolResolver *PoolResolver
105 rewardState *RewardState
106 warmupLen int
107
108 update positionRewardUpdate
109 }
110
111 // newPositionRewardContext resolves the deposit, its pool and a fresh RewardState WITHOUT mutating
112 // persisted state. A missing pool is created ephemerally and its persistence is deferred to the
113 // update path.
114 func (s *stakerV1) newPositionRewardContext(param *calculatePositionRewardParam) *positionRewardContext {
115 deposit := param.deposit()
116 poolPath := deposit.TargetPoolPath()
117
118 pool, poolExisted := param.Pools.Get(poolPath)
119 if !poolExisted {
120 // Read-only: use an ephemeral pool; persistence is deferred to updatePositionReward.
121 pool = sr.NewPool(poolPath, param.CurrentTime)
122 }
123 poolResolver := newPoolResolverWithExit(pool, param.Exit)
124
125 return &positionRewardContext{
126 deposit: deposit,
127 depositResolver: NewDepositResolver(deposit),
128 poolResolver: poolResolver,
129 rewardState: poolResolver.RewardStateOf(deposit),
130 warmupLen: len(deposit.Warmups()),
131 update: positionRewardUpdate{
132 poolPath: poolPath,
133 pool: pool,
134 poolExisted: poolExisted,
135 },
136 }
137 }
138
139 // newRewards allocates the empty per-warmup reward slice the compute* methods write into.
140 func (self *positionRewardContext) newRewards() []Reward {
141 rewards := make([]Reward, self.warmupLen)
142 for i := 0 ; i < self.warmupLen; i++ {
143 rewards[i] = Reward{
144 External: make( map [ string ] int64 ),
145 ExternalPenalty: make( map [ string ] int64 ),
146 }
147 }
148
149 return rewards
150 }
151
152 // computeInternalRewards writes the GNS emission reward and penalty of each warmup into rewards.
153 //
154 // The per-second emission-rate schedule is resolved up-front (resolveInternalRewardSegments) and fed to
155 // the single internal-reward calculator, so the calculation needs no reward-cache update.
156 func (self *positionRewardContext) computeInternalRewards(param *calculatePositionRewardParam, rewards []Reward) error {
157 lastCollectTime := self.depositResolver.InternalRewardLastCollectTime()
158
159 // Resolve the per-second reward-rate schedule (pure) and calculate internal rewards from it.
160 internalSegments, err := self.poolResolver.resolveInternalRewardSegments(param.PoolTier, self.update.poolPath, lastCollectTime, param.CurrentTime)
161 if err != nil {
162 return err
163 }
164 calculatedInternalRewards, calculatedInternalPenalties := self.rewardState.calculateInternalReward(internalSegments)
165
166 for i := 0 ; i < self.warmupLen; i++ {
167 rewards[i].Internal = calculatedInternalRewards[i]
168 rewards[i].InternalPenalty = calculatedInternalPenalties[i]
169 }
170
171 self.rewardState.reset()
172
173 return nil
174 }
175
176 // discoverExternalIncentives records the incentives created since the deposit's last update on the
177 // update, WITHOUT mutating the deposit. The collect path adds them to the deposit's incentive index.
178 //
179 // ExternalRewardLastCollectTime falls back to StakeTime for ids not yet persisted, so a newly-discovered
180 // incentive yields the same calculation whether or not it is written to the deposit.
181 func (self *positionRewardContext) discoverExternalIncentives(param *calculatePositionRewardParam) {
182 lastExternalIncentiveUpdatedAt := self.depositResolver.LastExternalIncentiveUpdatedAt()
183 if lastExternalIncentiveUpdatedAt >= param.CurrentTime {
184 return
185 }
186
187 // Discover incentives from this pool's own start-time index. Using the
188 // local resolver keeps calculation read-only even for an ephemeral pool
189 // that has not yet been persisted in param.Pools.
190 newIds := make([] string , 0 )
191 self.poolResolver.IncentivesResolver().IterateIncentiveIdsByTime(lastExternalIncentiveUpdatedAt, param.CurrentTime, func (incentiveId string ) bool {
192 newIds = append(newIds, incentiveId)
193 return false
194 })
195
196 self.update.newExternalIncentiveIds = newIds
197 self.update.advanceExternalIncentiveCursor = true
198 }
199
200 // externalIncentiveIds returns the deposit's effective incentive-id set: the ids stored on the deposit
201 // plus the ones discoverExternalIncentives found. Call it after discoverExternalIncentives.
202 func (self *positionRewardContext) externalIncentiveIds() [] string {
203 seen := make( map [ string ] bool )
204 incentiveIds := make([] string , 0 )
205
206 self.deposit.IterateExternalIncentiveIds( func (incentiveId string ) bool {
207 if !seen[incentiveId] {
208 seen[incentiveId] = true
209 incentiveIds = append(incentiveIds, incentiveId)
210 }
211 return false
212 })
213
214 for _, incentiveId := range self.update.newExternalIncentiveIds {
215 if !seen[incentiveId] {
216 seen[incentiveId] = true
217 incentiveIds = append(incentiveIds, incentiveId)
218 }
219 }
220
221 return incentiveIds
222 }
223
224 // computeExternalReward writes one incentive's external reward and penalty of each warmup into rewards.
225 // An incentive that does not exist on the pool or has not started yet contributes nothing.
226 func (self *positionRewardContext) computeExternalReward(param *calculatePositionRewardParam, rewards []Reward, incentiveId string ) {
227 incentive, ok := self.poolResolver.IncentivesResolver().Get(incentiveId)
228 if !ok {
229 return
230 }
231
232 incentiveResolver := NewExternalIncentiveResolver(incentive)
233
234 // Check if incentive is active during this specific collection period
235 if !incentiveResolver.IsStarted(param.CurrentTime) {
236 return
237 }
238
239 // External incentivized pool.
240 // Calculate reward for each warmup using per-incentive lastCollectTime
241 externalLastCollectTime := self.depositResolver.ExternalRewardLastCollectTime(incentiveId)
242 externalReward, externalPenalty := self.rewardState.calculateExternalReward(externalLastCollectTime, param.CurrentTime, incentive)
243
244 for i := range externalReward {
245 if externalReward[i] > 0 || externalPenalty[i] > 0 {
246 rewards[i].External[incentiveId] = externalReward[i]
247 rewards[i].ExternalPenalty[incentiveId] = externalPenalty[i]
248 }
249 }
250
251 self.update.externalIncentiveIds = append(self.update.externalIncentiveIds, incentiveId)
252 self.rewardState.reset()
253 }
254
255 // computeExternalRewards writes every incentive of the deposit into rewards.
256 func (self *positionRewardContext) computeExternalRewards(param *calculatePositionRewardParam, rewards []Reward) {
257 self.discoverExternalIncentives(param)
258
259 for _, incentiveId := range self.externalIncentiveIds() {
260 self.computeExternalReward(param, rewards, incentiveId)
261 }
262 }
263
264 // calculateCollectablePositionReward calculates the aggregated position reward WITHOUT mutating any persisted state.
265 //
266 // It is the shared, read-only entry point used by the Collectable* view getters.
267 // This keeps the calculation identical for every caller and guarantees views never write.
268 func (s *stakerV1) calculateCollectablePositionReward(currentHeight, currentTimestamp int64 , positionId uint64 ) (Reward, error ) {
269 param := &calculatePositionRewardParam{
270 CurrentHeight: currentHeight,
271 CurrentTime: currentTimestamp,
272 Deposits: s.getDeposits(),
273 Pools: s.getPools(),
274 PoolTier: s.getPoolTier(),
275 PositionId: positionId,
276 }
277
278 // An unstaked position quotes against its exit checkpoint, at its exit time.
279 if checkpoint := s.getUnstakedPositions().get(positionId); checkpoint != nil {
280 param.CurrentTime = checkpoint.ExitTime()
281 param.Deposit = checkpoint.Deposit()
282 param.Exit = checkpoint
283 }
284
285 rewards, _, err := s.calculatePositionReward(param)
286 if err != nil {
287 return Reward{}, err
288 }
289
290 return aggregateRewards(rewards), nil
291 }
292
293 // calculatePositionReward computes a position's per-warmup internal AND external rewards WITHOUT
294 // mutating persisted state. All would-be state changes are returned as a positionRewardUpdate for the
295 // caller to apply (collect only).
296 func (s *stakerV1) calculatePositionReward(param *calculatePositionRewardParam) ([]Reward, positionRewardUpdate, error ) {
297 ctx := s.newPositionRewardContext(param)
298 rewards := ctx.newRewards()
299
300 if err := ctx.computeInternalRewards(param, rewards); err != nil {
301 return nil , positionRewardUpdate{}, err
302 }
303 ctx.computeExternalRewards(param, rewards)
304
305 return rewards, ctx.update, nil
306 }
307
308 // calculateExternalPositionRewards computes all of a position's external incentive rewards WITHOUT
309 // mutating persisted state. Used by the combined collect path while GNS emission is halted.
310 func (s *stakerV1) calculateExternalPositionRewards(param *calculatePositionRewardParam) ([]Reward, positionRewardUpdate) {
311 ctx := s.newPositionRewardContext(param)
312 rewards := ctx.newRewards()
313
314 ctx.computeExternalRewards(param, rewards)
315
316 return rewards, ctx.update
317 }
318
319 // calculateInternalPositionReward computes only a position's per-warmup GNS emission rewards, WITHOUT
320 // mutating persisted state. Used by the emission collect path.
321 func (s *stakerV1) calculateInternalPositionReward(param *calculatePositionRewardParam) ([]Reward, positionRewardUpdate, error ) {
322 ctx := s.newPositionRewardContext(param)
323 rewards := ctx.newRewards()
324
325 if err := ctx.computeInternalRewards(param, rewards); err != nil {
326 return nil , positionRewardUpdate{}, err
327 }
328
329 return rewards, ctx.update, nil
330 }
331
332 // calculateExternalPositionReward computes a position's per-warmup reward for one external incentive,
333 // WITHOUT mutating persisted state. Used by the external incentive collect path.
334 //
335 // Incentive discovery still runs over every incentive, so the deposit's incentive index stays complete
336 // no matter which incentive the caller asked for.
337 func (s *stakerV1) calculateExternalPositionReward(param *calculatePositionRewardParam, incentiveId string ) ([]Reward, positionRewardUpdate) {
338 ctx := s.newPositionRewardContext(param)
339 rewards := ctx.newRewards()
340
341 ctx.discoverExternalIncentives(param)
342 ctx.computeExternalReward(param, rewards, incentiveId)
343
344 return rewards, ctx.update
345 }
346
347 // persistLazyPool persists a pool that calculation created ephemerally because it did not exist yet.
348 func (s *stakerV1) persistLazyPool(param *calculatePositionRewardParam, updateParams positionRewardUpdate) {
349 if !updateParams.poolExisted {
350 param.Pools.set(updateParams.poolPath, updateParams.pool)
351 }
352 }
353
354 // updateInternalRewardCache advances the reward cache up to CurrentTime (halving boundaries). The
355 // calculation itself no longer needs this, but downstream unclaimable processing (which reads
356 // CurrentReward) and off-chain history rely on the cache, so it is advanced on the emission collect path.
357 func (s *stakerV1) updateInternalRewardCache(param *calculatePositionRewardParam, updateParams positionRewardUpdate) {
358 // Always at the current time, never at param.CurrentTime: the cache and the unclaimable
359 // tracking it drives are pool-global, and a checkpoint collect would rewind them to its exit
360 // timestamp, where the pool may have had no staked liquidity.
361 param.PoolTier.cacheRewardForPool(time.Now().Unix(), param.Pools, updateParams.poolPath)
362 }
363
364 // applyExternalIncentiveIndex persists the deposit incentive-index updates discovered during calculation.
365 func (s *stakerV1) applyExternalIncentiveIndex(param *calculatePositionRewardParam, updateParams positionRewardUpdate) {
366 if len(updateParams.newExternalIncentiveIds) == 0 && !updateParams.advanceExternalIncentiveCursor {
367 return
368 }
369
370 deposit := param.deposit()
371 for _, incentiveId := range updateParams.newExternalIncentiveIds {
372 deposit.AddExternalIncentiveId(incentiveId)
373 }
374 if updateParams.advanceExternalIncentiveCursor {
375 deposit.SetLastExternalIncentiveUpdatedAt(param.CurrentTime)
376 }
377 }
378
379 // updatePositionReward applies the persisted-state changes produced by calculatePositionReward.
380 // It is called ONLY by the collect path; the Collectable* view getters discard the update.
381 func (s *stakerV1) updatePositionReward(param *calculatePositionRewardParam, updateParams positionRewardUpdate) {
382 s.persistLazyPool(param, updateParams)
383 s.updateInternalRewardCache(param, updateParams)
384 s.applyExternalIncentiveIndex(param, updateParams)
385 }
386
387 // updateInternalPositionReward applies the state changes belonging to the emission collect path only.
388 func (s *stakerV1) updateInternalPositionReward(param *calculatePositionRewardParam, updateParams positionRewardUpdate) {
389 s.persistLazyPool(param, updateParams)
390 s.updateInternalRewardCache(param, updateParams)
391 }
392
393 // updateExternalPositionReward applies the state changes belonging to the external collect path only.
394 func (s *stakerV1) updateExternalPositionReward(param *calculatePositionRewardParam, updateParams positionRewardUpdate) {
395 s.persistLazyPool(param, updateParams)
396 s.applyExternalIncentiveIndex(param, updateParams)
397 }
398
399 // internalRewardSegment is a [start, end) span over which the per-second emission reward rate is constant.
400 type internalRewardSegment struct {
401 start int64
402 end int64
403 rewardPerSecond int64
404 }
405
406 // resolveInternalRewardSegments builds the per-second reward-rate schedule over [startTime, endTime]
407 // WITHOUT mutating state.
408 //
409 // Persisted reward-cache entries cover the historical portion: they record tier/count changes and any
410 // halvings already materialized by past collects, so no un-materialized halving exists strictly between
411 // two persisted entries. The tail beyond the last persisted entry is split by halvings using the current
412 // tier ratio/count, which are necessarily constant there (a change would have written a cache entry). The
413 // tail rate is recomputed with the same calculatePoolReward arithmetic the cache writer uses, so a
414 // schedule resolved from a fully materialized cache and one resolved from the emission halvings are
415 // identical.
416 func (self *PoolResolver) resolveInternalRewardSegments(poolTier *PoolTier, poolPath string , startTime, endTime int64 ) ([]internalRewardSegment, error ) {
417 segments := make([]internalRewardSegment, 0 )
418 if startTime >= endTime {
419 return segments, nil
420 }
421
422 currentReward := self.CurrentReward(startTime)
423 cursor := startTime
424
425 self.RewardCache().Iterate(startTime, endTime, func (key int64 , value any ) bool {
426 reward, ok := value.( int64 )
427 if !ok {
428 panic(ufmt.Sprintf( "failed to cast value to int64: %T" , value))
429 }
430
431 segments = append(segments, internalRewardSegment{start: cursor, end: key, rewardPerSecond: currentReward})
432 cursor = key
433 currentReward = reward
434 return false
435 })
436
437 if cursor < endTime {
438 var err error
439 segments, err = appendInternalRewardTailSegments(segments, poolTier, poolPath, cursor, endTime, currentReward, self.exit)
440 if err != nil {
441 return nil , err
442 }
443 }
444
445 return segments, nil
446 }
447
448 // appendInternalRewardTailSegments appends the schedule for the tail [startTime, endTime], where no
449 // persisted cache entry exists beyond startTime. Over this span tier/count are constant, so the rate
450 // changes only at halving boundaries.
451 func appendInternalRewardTailSegments(segments []internalRewardSegment, poolTier *PoolTier, poolPath string , startTime, endTime, baseReward int64 , exit *sr.UnstakedPosition) ([]internalRewardSegment, error ) {
452 // A checkpoint reads the tier context it exited under: this window closed at the exit, and
453 // the live lookup is skipped so a checkpoint collect never depends on live tier state.
454 var tier, ratio, count uint64
455 if exit != nil {
456 tier, ratio, count = exit.Tier(), exit.TierRatio(), exit.TierCount()
457 } else {
458 var err error
459 tier, ratio, count, err = poolTier.tierContextOf(poolPath)
460 if err != nil {
461 return nil , err
462 }
463 }
464
465 if tier == 0 || tier >= AllTierCount {
466 // Not currently tiered: the rate cannot increase; the base is 0 after de-tier.
467 return append(segments, internalRewardSegment{start: startTime, end: endTime, rewardPerSecond: baseReward}), nil
468 }
469
470 tierRatioInt64 := int64 (ratio)
471 tierCount := int64 (count)
472
473 halvingTimestamps, halvingEmissions, err := poolTier.getHalvingBlocksInRange(startTime, endTime)
474 if err != nil {
475 return nil , err
476 }
477
478 segStart := startTime
479 rate := baseReward
480 for i, hv := range halvingTimestamps {
481 if hv <= segStart {
482 // Halving effective at/before the segment start: only switch the rate.
483 rate, err = calculatePoolReward(halvingEmissions[i], tierRatioInt64, tierCount)
484 if err != nil {
485 return nil , err
486 }
487 continue
488 }
489 if hv >= endTime {
490 break
491 }
492
493 segments = append(segments, internalRewardSegment{start: segStart, end: hv, rewardPerSecond: rate})
494 rate, err = calculatePoolReward(halvingEmissions[i], tierRatioInt64, tierCount)
495 if err != nil {
496 return nil , err
497 }
498 segStart = hv
499 }
500
501 return append(segments, internalRewardSegment{start: segStart, end: endTime, rewardPerSecond: rate}), nil
502 }
503
504 // calculates internal unclaimable reward for the pool
505 func (s *stakerV1) processUnClaimableReward(poolPath string , endTimestamp int64 ) int64 {
506 pool, ok := s.getPools().Get(poolPath)
507 if !ok {
508 return 0
509 }
510 poolResolver := NewPoolResolver(pool)
511
512 return poolResolver.processUnclaimableReward(endTimestamp)
513 }
514
515 // update deposit's incentive list with new incentives created since last update
516 func (s *stakerV1) getExternalIncentiveIdsBy(poolPath string , startTime, endTime int64 ) [] string {
517 currentIncentiveIds := make([] string , 0 )
518
519 pool, ok := s.getPools().Get(poolPath)
520 if !ok {
521 return currentIncentiveIds
522 }
523 poolResolver := NewPoolResolver(pool)
524
525 // Look up the pool's own start-time index instead of a global
526 // creation-time index. The index is scoped to this pool's incentives, so
527 // discovery cost is bounded by the number of incentives for this pool
528 // within the queried range, and no longer grows with the total number of
529 // incentives system-wide.
530 poolResolver.IncentivesResolver().IterateIncentiveIdsByTime(startTime, endTime, func (incentiveId string ) bool {
531 currentIncentiveIds = append(currentIncentiveIds, incentiveId)
532 return false
533 })
534
535 return currentIncentiveIds
536 }
537
538 // getInitialCollectTime determines the initial collection time for an incentive
539 // by taking the maximum of the deposit's stake time and the incentive's start time.
540 // This ensures rewards are only calculated from when both conditions are met:
541 // - The position must be staked (deposit.stakeTime)
542 // - The incentive must be active (incentive.startTimestamp)
543 //
544 // This function is used for lazy initialization when a position collects
545 // from an incentive for the first time, avoiding the need to iterate through
546 // all deposits when a new incentive is created.
547 func getInitialCollectTime(deposit *sr.Deposit, incentive *sr.ExternalIncentive) int64 {
548 if deposit.StakeTime() > incentive.StartTimestamp() {
549 return deposit.StakeTime()
550 }
551 return incentive.StartTimestamp()
552 }
553
*gno.land/r/gnoswap/staker/v1.stakerV1