Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}