Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}