Kind Realm (renderable)
Name v1
Namespace gnoswap / staker
Exported functions 14
Module gno.land/r/gnoswap/staker/v1
gno 0.9 reward_calculation_tick.gno gno
⧉
1 package staker 2 3 import ( 4 "chain" 5 "errors" 6 7 "gno.land/p/gnoswap/gnsmath/v1" 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.
i256 "gno.land/p/gnoswap/int256/v1"
9 u256 "gno.land/p/gnoswap/uint256/v1"
10 "gno.land/p/gnoswap/utils/v1"
11 sr "gno.land/r/gnoswap/staker"
12 )
13
14 type TickResolver struct {
15 *sr.Tick
16 }
17
18 // CurrentOutsideAccumulation returns the latest outside accumulation for the tick
19 // Parameters:
20 // - timestamp: Timestamp through which the tick's outside-accumulation history is queried.
21 //
22 // Returns:
23 // - *u256.Uint: Most recent decoded outside accumulation at or before timestamp, or zero when none exists.
24 func (self *TickResolver) CurrentOutsideAccumulation(timestamp int64 ) *u256.Uint {
25 var acc *u256.Uint
26 self.OutsideAccumulation().ReverseIterate( 0 , timestamp, func (key int64 , value any ) bool {
27 v, ok := value.( string )
28 if !ok {
29 panic( "failed to cast value to string" )
30 }
31 acc = utils.DecodeUint256(v)
32 return true
33 })
34 if acc == nil {
35 return u256.Zero()
36 }
37 return acc
38 }
39
40 // modifyDepositLower updates the tick's liquidity info by treating the deposit as a lower tick
41 func (self *TickResolver) modifyDepositLower(currentTime int64 , liquidity *i256.Int) {
42 // update staker side tick info
43 self.SetStakedLiquidityGross(gnsmath.LiquidityMathAddDelta(self.StakedLiquidityGross(), liquidity))
44 if self.StakedLiquidityGross().Lt(u256.Zero()) {
45 panic( "stakedLiquidityGross is negative" )
46 }
47 self.SetStakedLiquidityDelta(i256.Zero().Add(self.StakedLiquidityDelta(), liquidity))
48 }
49
50 // modifyDepositUpper updates the tick's liquidity info by treating the deposit as an upper tick
51 func (self *TickResolver) modifyDepositUpper(currentTime int64 , liquidity *i256.Int) {
52 self.SetStakedLiquidityGross(gnsmath.LiquidityMathAddDelta(self.StakedLiquidityGross(), liquidity))
53 if self.StakedLiquidityGross().Lt(u256.Zero()) {
54 panic( "stakedLiquidityGross is negative" )
55 }
56 self.SetStakedLiquidityDelta(i256.Zero().Sub(self.StakedLiquidityDelta(), liquidity))
57 }
58
59 // updateCurrentOutsideAccumulation updates the tick's outside accumulation
60 // It "flips" the accumulation's inside/outside by subtracting the current outside accumulation from the global accumulation.
61 // An unchanged result does not require a checkpoint because predecessor queries
62 // return the same value for this and all subsequent timestamps.
63 func (self *TickResolver) updateCurrentOutsideAccumulation(timestamp int64 , acc *u256.Uint) *u256.Uint {
64 currentOutsideAccumulation := self.CurrentOutsideAccumulation(timestamp)
65 newOutsideAccumulation := u256.Zero().Sub(acc, currentOutsideAccumulation)
66 if newOutsideAccumulation.Eq(currentOutsideAccumulation) {
67 return currentOutsideAccumulation
68 }
69
70 self.SetOutsideAccumulationAt(timestamp, newOutsideAccumulation)
71 return newOutsideAccumulation
72 }
73
74 // NewTickResolver wraps a staker tick to expose reward-accumulation updates.
75 //
76 // Parameters:
77 // - tick: Staker tick whose state is resolved.
78 //
79 // Returns:
80 // - *TickResolver: Resolver backed by tick.
81 func NewTickResolver(tick *sr.Tick) *TickResolver {
82 return &TickResolver{
83 Tick: tick,
84 }
85 }
86
87 // swapStartHook is called when a swap starts
88 // This hook initializes the batch processor for accumulating tick crosses
89 func (s *stakerV1) swapStartHook(_ int , rlm realm, poolPath string , timestamp int64 ) {
90 pool, ok := s.getPools().Get(poolPath)
91 if !ok {
92 return
93 }
94 if pool.Ticks().Tree().Size() == 0 {
95 return
96 }
97
98 // Initialize batch processor for this swap
99 // This will accumulate all tick crosses until swap completion
100 currentSwapBatch := sr.NewSwapBatchProcessor(poolPath, pool, timestamp)
101 err := s.store.SetCurrentSwapBatch( 0 , rlm, currentSwapBatch)
102 if err != nil {
103 panic(err)
104 }
105 }
106
107 // swapEndHook is called when a swap ends
108 // This hook processes all accumulated tick crosses in a single batch operation
109 // and cleans up the batch processor. The batch processing approach provides:
110 // 1. O(1) pool state updates instead of O(n) where n = number of tick crosses
111 // 2. Reduced computational overhead for reward calculations
112 // 3. Atomic processing ensuring consistency across all tick updates
113 func (s *stakerV1) swapEndHook(_ int , rlm realm, poolPath string ) error {
114 // Validate batch processor state
115 currentSwapBatch := s.store.GetCurrentSwapBatch()
116
117 if currentSwapBatch == nil || !currentSwapBatch.IsActive() || currentSwapBatch.PoolPath() != poolPath {
118 return nil
119 }
120
121 // Disable further accumulation
122 currentSwapBatch.SetIsActive( false )
123
124 // Process all accumulated tick crosses in a single batch
125 // This is where the optimization happens - instead of processing
126 // each tick cross individually, we calculate cumulative effects
127 err := s.processBatchedTickCrosses( 0 , rlm)
128 if err != nil {
129 return err
130 }
131
132 // Clean up batch processor
133 err = s.store.SetCurrentSwapBatch( 0 , rlm, nil )
134 if err != nil {
135 return err
136 }
137
138 return nil
139 }
140
141 // tickCrossHook is called when a tick is crossed.
142 // Active swaps accumulate crosses and process their net liquidity change once at
143 // swap end. Calls without an active batch use the immediate fallback, which
144 // materializes the reward cache for each individual tick cross.
145 func (s *stakerV1) tickCrossHook(_ int , rlm realm, poolPath string , tickId int32 , zeroForOne bool , timestamp int64 ) {
146 pool, ok := s.getPools().Get(poolPath)
147 if !ok {
148 return
149 }
150
151 // Skip ticks without staking state.
152 tick := pool.Ticks().Get(tickId)
153 if tick == nil {
154 return
155 }
156
157 // Skip ticks without staked boundary liquidity (no reward impact)
158 if tick.StakedLiquidityGross().IsZero() {
159 return
160 }
161
162 currentSwapBatch := s.store.GetCurrentSwapBatch()
163 // Batch processing path: accumulate tick crosses during active swap
164 if currentSwapBatch != nil && currentSwapBatch.IsActive() && currentSwapBatch.PoolPath() == poolPath {
165 // Pre-calculate liquidity delta with direction consideration
166 // zeroForOne swap: liquidity delta is negated (liquidity being removed from current tick)
167 liquidityDelta := tick.StakedLiquidityDelta()
168 if zeroForOne {
169 liquidityDelta = i256.Zero().Neg(liquidityDelta)
170 }
171
172 // Accumulate this tick cross for batch processing
173 currentSwapBatch.AddCross(sr.NewSwapTickCross(tickId, zeroForOne, liquidityDelta))
174 return
175 }
176
177 // Immediate fallback for tick-cross callers without an active swap batch.
178 // Current production tick crosses originate from Swap and use batching; this
179 // path preserves correct accounting for future non-swap callers.
180 s.processTickCrossImmediate(pool, tick, tickId, zeroForOne, timestamp)
181 }
182
183 // processTickCrossImmediate processes one unbatched tick cross.
184 // It materializes elapsed halving boundaries while the pre-cross liquidity is
185 // still effective, before modifying the deposit for that individual cross.
186 func (s *stakerV1) processTickCrossImmediate(pool *sr.Pool, tick *sr.Tick, tickId int32 , zeroForOne bool , timestamp int64 ) {
187 // Calculate the effective tick position after crossing
188 // For zeroForOne swaps, liquidity becomes effective one tick lower
189 nextTick := tickId
190 if zeroForOne {
191 nextTick-- // Move to the lower tick where liquidity becomes active
192 }
193
194 // Calculate liquidity delta with direction consideration
195 liquidityDelta := tick.StakedLiquidityDelta()
196 if zeroForOne {
197 // Negate delta for zeroForOne direction (liquidity being removed from current range)
198 liquidityDelta = i256.Zero().Neg(liquidityDelta)
199 }
200
201 // Update pool's cumulative deposit with the liquidity change
202 poolResolver := NewPoolResolver(pool)
203 s.getPoolTier().cacheRewardForPool(timestamp, s.getPools(), pool.PoolPath())
204 newAcc := poolResolver.modifyDeposit(liquidityDelta, timestamp, nextTick)
205
206 // Update the tick's outside accumulation for reward calculations
207 // This ensures proper reward distribution tracking across tick boundaries
208 tickResolver := NewTickResolver(tick)
209 tickResolver.updateCurrentOutsideAccumulation(timestamp, newAcc)
210 }
211
212 // processBatchedTickCrosses processes all accumulated tick crosses at once
213 // This is the core optimization function that processes multiple tick crosses in a single operation.
214 // Instead of updating pool state for each tick cross individually (O(n) operations),
215 // it calculates the cumulative effect and applies it once (O(1) pool updates + O(n) tick updates).
216 func (s *stakerV1) processBatchedTickCrosses(_ int , rlm realm) error {
217 // Early exit for empty batches
218 currentSwapBatch := s.store.GetCurrentSwapBatch()
219 if currentSwapBatch == nil || len(currentSwapBatch.Crosses()) == 0 {
220 return nil
221 }
222
223 // Validate pool reference
224 if currentSwapBatch.Pool() == nil {
225 return errors.New(errPoolNotFound)
226 }
227
228 batch := currentSwapBatch
229 timestamp := batch.Timestamp()
230
231 // Phase 1: Calculate cumulative liquidity delta across all tick crosses
232 // This replaces multiple individual pool updates with a single cumulative update
233 cumulativeDelta := i256.Zero()
234 for _, tickCross := range batch.Crosses() {
235 newDelta := cumulativeDelta.Add(cumulativeDelta, tickCross.Delta())
236 cumulativeDelta = newDelta
237 }
238
239 // Phase 2: Determine the effective tick position for pool state update
240 // Use the last crossed tick as the reference point for cumulative changes
241 lastCross := batch.LastCross()
242 if lastCross == nil {
243 return nil
244 }
245
246 lastTick := lastCross.TickID()
247 if lastCross.ZeroForOne() {
248 lastTick-- // Adjust for zeroForOne direction
249 }
250
251 // Phase 3: Apply the batch's cumulative change to pool state once.
252 // cacheRewardForPool runs once for each non-empty normal swap batch, before
253 // the net liquidity change. It sees the pre-cross liquidity and splits
254 // unclaimable intervals at every elapsed halving boundary.
255 poolResolver := NewPoolResolver(batch.Pool())
256 s.getPoolTier().cacheRewardForPool(timestamp, s.getPools(), batch.PoolPath())
257 newAcc := poolResolver.modifyDeposit(cumulativeDelta, timestamp, lastTick)
258
259 // Phase 4: Update individual tick outside accumulations for reward tracking
260 // While we optimize pool updates, each tick still needs its accumulation updated
261 // for proper reward distribution calculations
262
263 for _, tickCross := range batch.Crosses() {
264 tick := batch.Pool().Ticks().Get(tickCross.TickID())
265 if tick == nil {
266 // Pruned after the cross was accumulated, so its staked gross
267 // liquidity is zero and it carries no reward weight.
268 continue
269 }
270
271 tickResolver := NewTickResolver(tick)
272 outsideAccumulation := tickResolver.updateCurrentOutsideAccumulation(timestamp, newAcc)
273
274 tickCrossEventInfo := NewTickCrossEventInfo(
275 tickCross.TickID(),
276 tick.StakedLiquidityGross(),
277 tick.StakedLiquidityDelta(),
278 outsideAccumulation,
279 )
280
281 chain.Emit(
282 "StakerTickCross" ,
283 "poolPath" , batch.PoolPath(),
284 "tick" , tickCrossEventInfo.ToString(),
285 )
286 }
287
288 previousRealm := rlm.Previous()
289 stakedLiquidity := poolResolver.CurrentStakedLiquidity(timestamp)
290
291 // Emit event with staker-side tick cross information.
292 // lastTick — the effective tick written to HistoricalTick by modifyDeposit above.
293 // Reward calculation (CalculateRawRewardForPosition) reads HistoricalTick for the feeGrowthInside branch,
294 // so off-chain indexers must use this same value (NOT the pool's Slot0 tick) to reproduce in-range status.
295 chain.Emit(
296 "BatchStakerTickCross" ,
297 "prevAddr" , previousRealm.Address().String(),
298 "prevRealm" , previousRealm.PkgPath(),
299 "poolPath" , batch.PoolPath(),
300 "blockTimestamp" , utils.FormatInt(timestamp),
301 "stakedLiquidity" , stakedLiquidity.ToString(),
302 "globalRewardRatioAccX128" , newAcc.ToString(),
303 "lastTick" , utils.FormatInt(lastTick),
304 )
305
306 return nil
307 }
308
309 func (s *stakerV1) setupSwapHooks(_ int , rlm realm) {
310 // Set tick cross hook for pool contract
311 s.poolAccessor.SetTickCrossHook( 0 , rlm, s.tickCrossHook)
312
313 // Set swap start/end hooks for batch processing
314 s.poolAccessor.SetSwapStartHook( 0 , rlm, s.swapStartHook)
315
316 // Set swap end hook for batch processing
317 s.poolAccessor.SetSwapEndHook( 0 , rlm, s.swapEndHook)
318 }
319
*gno.land/r/gnoswap/staker/v1.stakerV1