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