Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

reward_calculation_tick.gno

10.56 Kb · 295 lines
  1package staker
  2
  3import (
  4	"chain"
  5	"errors"
  6
  7	"gno.land/p/gnoswap/gnsmath"
  8	i256 "gno.land/p/gnoswap/int256"
  9	u256 "gno.land/p/gnoswap/uint256"
 10	"gno.land/p/gnoswap/utils"
 11	sr "gno.land/r/gnoswap/staker"
 12)
 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(u256.Zero()) {
 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(u256.Zero()) {
 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	if pool.Ticks().Tree().Size() == 0 {
 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(0, rlm, 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(_ int, rlm realm, 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(0, rlm)
109	if err != nil {
110		return err
111	}
112
113	// Clean up batch processor
114	err = s.store.SetCurrentSwapBatch(0, rlm, 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(_ int, rlm realm, poolPath string, tickId int32, zeroForOne bool, timestamp int64) {
128	pool, ok := s.getPools().Get(poolPath)
129	if !ok {
130		return
131	}
132
133	// Skip ticks without staking state.
134	tick := pool.Ticks().Get(tickId)
135	if tick == nil {
136		return
137	}
138
139	// Skip ticks without staked boundary liquidity (no reward impact)
140	if tick.StakedLiquidityGross().IsZero() {
141		return
142	}
143
144	currentSwapBatch := s.store.GetCurrentSwapBatch()
145	// Batch processing path: accumulate tick crosses during active swap
146	if currentSwapBatch != nil && currentSwapBatch.IsActive() && currentSwapBatch.PoolPath() == poolPath {
147		// Pre-calculate liquidity delta with direction consideration
148		// zeroForOne swap: liquidity delta is negated (liquidity being removed from current tick)
149		liquidityDelta := tick.StakedLiquidityDelta()
150		if zeroForOne {
151			liquidityDelta = i256.Zero().Neg(liquidityDelta)
152		}
153
154		// Accumulate this tick cross for batch processing
155		currentSwapBatch.AddCross(sr.NewSwapTickCross(tickId, zeroForOne, liquidityDelta))
156		return
157	}
158
159	// Immediate processing path: handle tick crosses outside of swap context
160	// This ensures real-time updates for non-swap operations (e.g., position modifications)
161	s.processTickCrossImmediate(pool, tick, tickId, zeroForOne, timestamp)
162}
163
164// processTickCrossImmediate processes a single tick cross immediately
165// This function handles individual tick crosses for non-swap operations
166// where batch processing is not applicable (e.g., position modifications, liquidations)
167func (s *stakerV1) processTickCrossImmediate(pool *sr.Pool, tick *sr.Tick, tickId int32, zeroForOne bool, timestamp int64) {
168	// Calculate the effective tick position after crossing
169	// For zeroForOne swaps, liquidity becomes effective one tick lower
170	nextTick := tickId
171	if zeroForOne {
172		nextTick-- // Move to the lower tick where liquidity becomes active
173	}
174
175	// Calculate liquidity delta with direction consideration
176	liquidityDelta := tick.StakedLiquidityDelta()
177	if zeroForOne {
178		// Negate delta for zeroForOne direction (liquidity being removed from current range)
179		liquidityDelta = i256.Zero().Neg(liquidityDelta)
180	}
181
182	// Update pool's cumulative deposit with the liquidity change
183	poolResolver := NewPoolResolver(pool)
184	newAcc := poolResolver.modifyDeposit(liquidityDelta, timestamp, nextTick)
185
186	// Update the tick's outside accumulation for reward calculations
187	// This ensures proper reward distribution tracking across tick boundaries
188	tickResolver := NewTickResolver(tick)
189	tickResolver.updateCurrentOutsideAccumulation(timestamp, newAcc)
190}
191
192// processBatchedTickCrosses processes all accumulated tick crosses at once
193// This is the core optimization function that processes multiple tick crosses in a single operation.
194// Instead of updating pool state for each tick cross individually (O(n) operations),
195// it calculates the cumulative effect and applies it once (O(1) pool updates + O(n) tick updates).
196func (s *stakerV1) processBatchedTickCrosses(_ int, rlm realm) error {
197	// Early exit for empty batches
198	currentSwapBatch := s.store.GetCurrentSwapBatch()
199	if currentSwapBatch == nil || len(currentSwapBatch.Crosses()) == 0 {
200		return nil
201	}
202
203	// Validate pool reference
204	if currentSwapBatch.Pool() == nil {
205		return errors.New(errPoolNotFound)
206	}
207
208	batch := currentSwapBatch
209	timestamp := batch.Timestamp()
210
211	// Phase 1: Calculate cumulative liquidity delta across all tick crosses
212	// This replaces multiple individual pool updates with a single cumulative update
213	cumulativeDelta := i256.Zero()
214	for _, tickCross := range batch.Crosses() {
215		newDelta := cumulativeDelta.Add(cumulativeDelta, tickCross.Delta())
216		cumulativeDelta = newDelta
217	}
218
219	// Phase 2: Determine the effective tick position for pool state update
220	// Use the last crossed tick as the reference point for cumulative changes
221	lastCross := batch.LastCross()
222	if lastCross == nil {
223		return nil
224	}
225
226	lastTick := lastCross.TickID()
227	if lastCross.ZeroForOne() {
228		lastTick-- // Adjust for zeroForOne direction
229	}
230
231	// Phase 3: Apply cumulative changes to pool state in a single operation
232	// This is the key optimization - one pool update instead of many
233	poolResolver := NewPoolResolver(batch.Pool())
234	newAcc := poolResolver.modifyDeposit(cumulativeDelta, timestamp, lastTick)
235
236	// Phase 4: Update individual tick outside accumulations for reward tracking
237	// While we optimize pool updates, each tick still needs its accumulation updated
238	// for proper reward distribution calculations
239
240	for _, tickCross := range batch.Crosses() {
241		tick := batch.Pool().Ticks().Get(tickCross.TickID())
242		if tick == nil {
243			// Pruned after the cross was accumulated, so its staked gross
244			// liquidity is zero and it carries no reward weight.
245			continue
246		}
247
248		tickResolver := NewTickResolver(tick)
249		tickResolver.updateCurrentOutsideAccumulation(timestamp, newAcc)
250
251		tickCrossEventInfo := NewTickCrossEventInfo(
252			tickCross.TickID(),
253			tick.StakedLiquidityGross(),
254			tick.StakedLiquidityDelta(),
255			tickResolver.CurrentOutsideAccumulation(timestamp),
256		)
257
258		chain.Emit(
259			"StakerTickCross",
260			"poolPath", batch.PoolPath(),
261			"tick", tickCrossEventInfo.ToString(),
262		)
263	}
264
265	previousRealm := rlm.Previous()
266	stakedLiquidity := poolResolver.CurrentStakedLiquidity(timestamp)
267
268	// Emit event with staker-side tick cross information.
269	// lastTick — the effective tick written to HistoricalTick by modifyDeposit above.
270	// Reward calculation (CalculateRawRewardForPosition) reads HistoricalTick for the feeGrowthInside branch,
271	// so off-chain indexers must use this same value (NOT the pool's Slot0 tick) to reproduce in-range status.
272	chain.Emit(
273		"BatchStakerTickCross",
274		"prevAddr", previousRealm.Address().String(),
275		"prevRealm", previousRealm.PkgPath(),
276		"poolPath", batch.PoolPath(),
277		"blockTimestamp", utils.FormatInt(timestamp),
278		"stakedLiquidity", stakedLiquidity.ToString(),
279		"globalRewardRatioAccX128", newAcc.ToString(),
280		"lastTick", utils.FormatInt(lastTick),
281	)
282
283	return nil
284}
285
286func (s *stakerV1) setupSwapHooks(_ int, rlm realm) {
287	// Set tick cross hook for pool contract
288	s.poolAccessor.SetTickCrossHook(0, rlm, s.tickCrossHook)
289
290	// Set swap start/end hooks for batch processing
291	s.poolAccessor.SetSwapStartHook(0, rlm, s.swapStartHook)
292
293	// Set swap end hook for batch processing
294	s.poolAccessor.SetSwapEndHook(0, rlm, s.swapEndHook)
295}