reward_calculation_tick.gno
9.92 Kb · 278 lines
1package v1
2
3import (
4 "chain"
5
6 "gno.land/p/gnoswap/gnsmath"
7 i256 "gno.land/p/gnoswap/int256"
8 u256 "gno.land/p/gnoswap/uint256"
9 sr "gno.land/r/gnoswap/staker"
10)
11
12var zeroUint256 = u256.Zero()
13
14type TickResolver struct {
15 *sr.Tick
16}
17
18// CurrentOutsideAccumulation returns the latest outside accumulation for the tick
19func (self *TickResolver) CurrentOutsideAccumulation(timestamp int64) *u256.Uint {
20 acc := u256.Zero()
21 self.OutsideAccumulation().ReverseIterate(0, timestamp, func(key int64, value any) bool {
22 v, ok := value.(*u256.Uint)
23 if !ok {
24 panic("failed to cast value to *u256.Uint")
25 }
26 acc = v
27 return true
28 })
29 if acc == nil {
30 acc = u256.Zero()
31 }
32 return acc
33}
34
35// modifyDepositLower updates the tick's liquidity info by treating the deposit as a lower tick
36func (self *TickResolver) modifyDepositLower(currentTime int64, liquidity *i256.Int) {
37 // update staker side tick info
38 self.SetStakedLiquidityGross(gnsmath.LiquidityMathAddDelta(self.StakedLiquidityGross(), liquidity))
39 if self.StakedLiquidityGross().Lt(zeroUint256) {
40 panic("stakedLiquidityGross is negative")
41 }
42 self.SetStakedLiquidityDelta(i256.Zero().Add(self.StakedLiquidityDelta(), liquidity))
43}
44
45// modifyDepositUpper updates the tick's liquidity info by treating the deposit as an upper tick
46func (self *TickResolver) modifyDepositUpper(currentTime int64, liquidity *i256.Int) {
47 self.SetStakedLiquidityGross(gnsmath.LiquidityMathAddDelta(self.StakedLiquidityGross(), liquidity))
48 if self.StakedLiquidityGross().Lt(zeroUint256) {
49 panic("stakedLiquidityGross is negative")
50 }
51 self.SetStakedLiquidityDelta(i256.Zero().Sub(self.StakedLiquidityDelta(), liquidity))
52}
53
54// updateCurrentOutsideAccumulation updates the tick's outside accumulation
55// It "flips" the accumulation's inside/outside by subtracting the current outside accumulation from the global accumulation
56func (self *TickResolver) updateCurrentOutsideAccumulation(timestamp int64, acc *u256.Uint) {
57 currentOutsideAccumulation := self.CurrentOutsideAccumulation(timestamp)
58 newOutsideAccumulation := u256.Zero().Sub(acc, currentOutsideAccumulation)
59 self.SetOutsideAccumulationAt(timestamp, newOutsideAccumulation)
60}
61
62func NewTickResolver(tick *sr.Tick) *TickResolver {
63 return &TickResolver{
64 Tick: tick,
65 }
66}
67
68// swapStartHook is called when a swap starts
69// This hook initializes the batch processor for accumulating tick crosses
70func (s *stakerV1) swapStartHook(_ int, rlm realm, poolPath string, timestamp int64) {
71 pool, ok := s.getPools().Get(poolPath)
72 if !ok {
73 return
74 }
75
76 // Initialize batch processor for this swap
77 // This will accumulate all tick crosses until swap completion
78 currentSwapBatch := sr.NewSwapBatchProcessor(poolPath, pool, timestamp)
79 err := s.store.SetCurrentSwapBatch(0, rlm, currentSwapBatch)
80 if err != nil {
81 panic(err)
82 }
83}
84
85// swapEndHook is called when a swap ends
86// This hook processes all accumulated tick crosses in a single batch operation
87// and cleans up the batch processor. The batch processing approach provides:
88// 1. O(1) pool state updates instead of O(n) where n = number of tick crosses
89// 2. Reduced computational overhead for reward calculations
90// 3. Atomic processing ensuring consistency across all tick updates
91func (s *stakerV1) swapEndHook(_ int, rlm realm, poolPath string) error {
92 // Validate batch processor state
93 currentSwapBatch := s.store.GetCurrentSwapBatch()
94
95 if currentSwapBatch == nil || !currentSwapBatch.IsActive() || currentSwapBatch.PoolPath() != poolPath {
96 return nil
97 }
98
99 // Disable further accumulation
100 currentSwapBatch.SetIsActive(false)
101
102 // Process all accumulated tick crosses in a single batch
103 // This is where the optimization happens - instead of processing
104 // each tick cross individually, we calculate cumulative effects
105 err := s.processBatchedTickCrosses(0, rlm)
106 if err != nil {
107 return err
108 }
109
110 // Clean up batch processor
111 err = s.store.SetCurrentSwapBatch(0, rlm, nil)
112 if err != nil {
113 return err
114 }
115
116 return nil
117}
118
119// tickCrossHook is called when a tick is crossed
120// This hook implements intelligent routing between batch processing and immediate processing:
121// - During swaps: accumulates tick crosses for batch processing at swap end
122// - Outside swaps: processes tick crosses immediately for real-time updates
123// The hybrid approach optimizes for both swap performance and non-swap responsiveness
124func (s *stakerV1) tickCrossHook(_ int, rlm realm, poolPath string, tickId int32, zeroForOne bool, timestamp int64) {
125 pool, ok := s.getPools().Get(poolPath)
126 if !ok {
127 return
128 }
129
130 tick := pool.Ticks().Get(tickId)
131
132 // Skip ticks with zero staked liquidity (no reward impact)
133 if tick.StakedLiquidityDelta().Sign() == 0 {
134 return
135 }
136
137 currentSwapBatch := s.store.GetCurrentSwapBatch()
138 // Batch processing path: accumulate tick crosses during active swap
139 if currentSwapBatch != nil && currentSwapBatch.IsActive() && currentSwapBatch.PoolPath() == poolPath {
140 // Pre-calculate liquidity delta with direction consideration
141 // zeroForOne swap: liquidity delta is negated (liquidity being removed from current tick)
142 liquidityDelta := tick.StakedLiquidityDelta()
143 if zeroForOne {
144 liquidityDelta = i256.Zero().Neg(liquidityDelta)
145 }
146
147 // Accumulate this tick cross for batch processing
148 currentSwapBatch.AddCross(sr.NewSwapTickCross(tickId, zeroForOne, liquidityDelta))
149 return
150 }
151
152 // Immediate processing path: handle tick crosses outside of swap context
153 // This ensures real-time updates for non-swap operations (e.g., position modifications)
154 s.processTickCrossImmediate(pool, tick, tickId, zeroForOne, timestamp)
155}
156
157// processTickCrossImmediate processes a single tick cross immediately
158// This function handles individual tick crosses for non-swap operations
159// where batch processing is not applicable (e.g., position modifications, liquidations)
160func (s *stakerV1) processTickCrossImmediate(pool *sr.Pool, tick *sr.Tick, tickId int32, zeroForOne bool, timestamp int64) {
161 // Calculate the effective tick position after crossing
162 // For zeroForOne swaps, liquidity becomes effective one tick lower
163 nextTick := tickId
164 if zeroForOne {
165 nextTick-- // Move to the lower tick where liquidity becomes active
166 }
167
168 // Calculate liquidity delta with direction consideration
169 liquidityDelta := tick.StakedLiquidityDelta()
170 if zeroForOne {
171 // Negate delta for zeroForOne direction (liquidity being removed from current range)
172 liquidityDelta = i256.Zero().Neg(liquidityDelta)
173 }
174
175 // Update pool's cumulative deposit with the liquidity change
176 poolResolver := NewPoolResolver(pool)
177 newAcc := poolResolver.modifyDeposit(liquidityDelta, timestamp, nextTick)
178
179 // Update the tick's outside accumulation for reward calculations
180 // This ensures proper reward distribution tracking across tick boundaries
181 tickResolver := NewTickResolver(tick)
182 tickResolver.updateCurrentOutsideAccumulation(timestamp, newAcc)
183}
184
185// processBatchedTickCrosses processes all accumulated tick crosses at once
186// This is the core optimization function that processes multiple tick crosses in a single operation.
187// Instead of updating pool state for each tick cross individually (O(n) operations),
188// it calculates the cumulative effect and applies it once (O(1) pool updates + O(n) tick updates).
189func (s *stakerV1) processBatchedTickCrosses(_ int, rlm realm) error {
190 // Early exit for empty batches
191 currentSwapBatch := s.store.GetCurrentSwapBatch()
192 if currentSwapBatch == nil || len(currentSwapBatch.Crosses()) == 0 {
193 return nil
194 }
195
196 // Validate pool reference
197 if currentSwapBatch.Pool() == nil {
198 return errPoolNotFound
199 }
200
201 batch := currentSwapBatch
202 timestamp := batch.Timestamp()
203
204 // Phase 1: Calculate cumulative liquidity delta across all tick crosses
205 // This replaces multiple individual pool updates with a single cumulative update
206 cumulativeDelta := i256.Zero()
207 for _, tickCross := range batch.Crosses() {
208 newDelta := cumulativeDelta.Add(cumulativeDelta, tickCross.Delta())
209 cumulativeDelta = newDelta
210 }
211
212 // Phase 2: Determine the effective tick position for pool state update
213 // Use the last crossed tick as the reference point for cumulative changes
214 lastCross := batch.LastCross()
215 if lastCross == nil {
216 return nil
217 }
218
219 lastTick := lastCross.TickID()
220 if lastCross.ZeroForOne() {
221 lastTick-- // Adjust for zeroForOne direction
222 }
223
224 // Phase 3: Apply cumulative changes to pool state in a single operation
225 // This is the key optimization - one pool update instead of many
226 poolResolver := NewPoolResolver(batch.Pool())
227 newAcc := poolResolver.modifyDeposit(cumulativeDelta, timestamp, lastTick)
228
229 // Phase 4: Update individual tick outside accumulations for reward tracking
230 // While we optimize pool updates, each tick still needs its accumulation updated
231 // for proper reward distribution calculations
232
233 for _, tickCross := range batch.Crosses() {
234 tick := batch.Pool().Ticks().Get(tickCross.TickID())
235 tickResolver := NewTickResolver(tick)
236 tickResolver.updateCurrentOutsideAccumulation(timestamp, newAcc)
237
238 tickCrossEventInfo := NewTickCrossEventInfo(
239 tickCross.TickID(),
240 tick.StakedLiquidityGross(),
241 tick.StakedLiquidityDelta(),
242 tickResolver.CurrentOutsideAccumulation(timestamp),
243 )
244
245 chain.Emit(
246 "StakerTickCross",
247 "poolPath", batch.PoolPath(),
248 "tick", tickCrossEventInfo.ToString(),
249 )
250 }
251
252 // Emit event with staker-side tick cross information
253 previousRealm := rlm.Previous()
254 stakedLiquidity := poolResolver.CurrentStakedLiquidity(timestamp)
255
256 chain.Emit(
257 "BatchStakerTickCross",
258 "prevAddr", previousRealm.Address().String(),
259 "prevRealm", previousRealm.PkgPath(),
260 "poolPath", batch.PoolPath(),
261 "blockTimestamp", formatAnyInt(timestamp),
262 "stakedLiquidity", stakedLiquidity.ToString(),
263 "globalRewardRatioAccX128", newAcc.ToString(),
264 )
265
266 return nil
267}
268
269func (s *stakerV1) setupSwapHooks(_ int, rlm realm) {
270 // Set tick cross hook for pool contract
271 s.poolAccessor.SetTickCrossHook(0, rlm, s.tickCrossHook)
272
273 // Set swap start/end hooks for batch processing
274 s.poolAccessor.SetSwapStartHook(0, rlm, s.swapStartHook)
275
276 // Set swap end hook for batch processing
277 s.poolAccessor.SetSwapEndHook(0, rlm, s.swapEndHook)
278}