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

swap.gno

27.97 Kb · 855 lines
  1package pool
  2
  3import (
  4	"chain"
  5	"errors"
  6	"strconv"
  7	"time"
  8
  9	ufmt "gno.land/p/nt/ufmt/v0"
 10	"gno.land/r/gnoswap/access/v1"
 11	"gno.land/r/gnoswap/halt/v1"
 12
 13	"gno.land/p/gnoswap/consts/v1"
 14	"gno.land/p/gnoswap/gnsmath/v1"
 15	i256 "gno.land/p/gnoswap/int256/v1"
 16	u256 "gno.land/p/gnoswap/uint256/v1"
 17	"gno.land/p/gnoswap/utils/v1"
 18
 19	pl "gno.land/r/gnoswap/pool"
 20)
 21
 22// Hook functions allow external contracts to be notified of swap events.
 23var (
 24	// MUST BE IMMUTABLE.
 25	// DO NOT USE THIS VALUE IN ANY ARITHMETIC OPERATIONS' INITIALIZATION
 26	zero           = u256.Zero()
 27	zeroI256       = i256.Zero() /* readonly */
 28	fixedPointQ128 = u256.MustFromDecimal(Q128)
 29
 30	maxInt256 = u256.MustFromDecimal(MAX_INT256)
 31	maxInt64  = i256.Zero().SetInt64(INT64_MAX)
 32	minInt64  = i256.Zero().SetInt64(INT64_MIN)
 33)
 34
 35// SetTickCrossHook sets the hook function called when a tick is crossed during swaps.
 36//
 37// Allows staker to monitor liquidity changes at price levels.
 38// Used for reward calculation when positions enter/exit range.
 39//
 40// Only callable by staker contract.
 41// Parameters:
 42//   - _: Leading integer discriminator for the forwarded realm call; callers
 43//     pass 0.
 44//   - rlm: Propagated realm context validated as current before the hook is
 45//     stored.
 46//   - hook: Callback invoked after an initialized tick is crossed with the
 47//     current pool realm, pool path, tick index, swap direction, and timestamp.
 48func (i *poolV1) SetTickCrossHook(_ int, rlm realm, hook func(cur realm, poolPath string, tickId int32, zeroForOne bool, timestamp int64)) {
 49	access.AssertIsRlmCurrent(0, rlm)
 50
 51	i.assertPoolUnlocked()
 52
 53	caller := rlm.Previous().Address()
 54	access.AssertIsStaker(caller)
 55
 56	i.lockPool(0, rlm)
 57	defer i.unlockPool(0, rlm)
 58
 59	err := i.store.SetTickCrossHook(0, rlm, hook)
 60	if err != nil {
 61		panic(err)
 62	}
 63}
 64
 65// SetSwapStartHook sets the hook function called at the beginning of a swap.
 66//
 67// Enables pre-swap state tracking for reward distribution.
 68// Captures timestamp for time-weighted calculations.
 69//
 70// Only callable by staker contract.
 71//
 72// Parameters:
 73//   - _: Leading integer discriminator for the forwarded realm call; callers
 74//     pass 0.
 75//   - rlm: Propagated realm context validated as current before the hook is
 76//     stored.
 77//   - hook: Callback invoked before swap computation with the current pool
 78//     realm, pool path, and block timestamp.
 79func (i *poolV1) SetSwapStartHook(_ int, rlm realm, hook func(cur realm, poolPath string, timestamp int64)) {
 80	access.AssertIsRlmCurrent(0, rlm)
 81
 82	i.assertPoolUnlocked()
 83
 84	caller := rlm.Previous().Address()
 85	access.AssertIsStaker(caller)
 86
 87	i.lockPool(0, rlm)
 88	defer i.unlockPool(0, rlm)
 89
 90	err := i.store.SetSwapStartHook(0, rlm, hook)
 91	if err != nil {
 92		panic(err)
 93	}
 94}
 95
 96// SetSwapEndHook sets the hook function called at the end of a swap.
 97//
 98// Finalizes reward calculations after swap completion.
 99// Allows error propagation to revert invalid swaps.
100//
101// Only callable by staker contract.
102//
103// Parameters:
104//   - _: Leading integer discriminator for the forwarded realm call; callers
105//     pass 0.
106//   - rlm: Propagated realm context validated as current before the hook is
107//     stored.
108//   - hook: Callback invoked after swap settlement with the current pool realm
109//     and pool path; a non-nil error aborts the surrounding swap.
110func (i *poolV1) SetSwapEndHook(_ int, rlm realm, hook func(cur realm, poolPath string) error) {
111	access.AssertIsRlmCurrent(0, rlm)
112
113	i.assertPoolUnlocked()
114
115	caller := rlm.Previous().Address()
116	access.AssertIsStaker(caller)
117
118	i.lockPool(0, rlm)
119	defer i.unlockPool(0, rlm)
120
121	err := i.store.SetSwapEndHook(0, rlm, hook)
122	if err != nil {
123		panic(err)
124	}
125}
126
127// SwapResult encapsulates all state changes from a swap.
128// It ensures atomic state transitions that can be applied at once.
129type SwapResult struct {
130	Amount0              *i256.Int
131	Amount1              *i256.Int
132	NewSqrtPrice         *u256.Uint
133	NewTick              int32
134	NewLiquidity         *u256.Uint
135	NewProtocolFeeToken0 int64
136	NewProtocolFeeToken1 int64
137	FeeGrowthGlobal0X128 *u256.Uint
138	FeeGrowthGlobal1X128 *u256.Uint
139}
140
141// SwapComputation encapsulates the pure computation logic for swaps.
142type SwapComputation struct {
143	AmountSpecified   *i256.Int
144	SqrtPriceLimitX96 *u256.Uint
145	ZeroForOne        bool
146	ExactInput        bool
147	InitialState      SwapState
148	Cache             *SwapCache
149}
150
151// Swap executes a swap with callback pattern for optimistic transfers.
152// This allows flash swaps where tokens are sent before payment is received.
153//
154// The flow is:
155// 1. Pool sends output tokens to recipient
156// 2. Pool calls callback on msg.sender
157// 3. Callback must ensure pool receives input tokens
158// 4. Pool validates its balance increased correctly
159//
160// Parameters:
161//   - _: Leading integer discriminator for the forwarded realm call; callers
162//     pass 0.
163//   - rlm: Current propagated realm context; it is validated as current and
164//     used for token transfers and callbacks.
165//   - token0Path: Registered path of token0; it must be the first token in the
166//     pool's canonical token ordering.
167//   - token1Path: Registered path of token1; it must be the second token in
168//     the pool's canonical token ordering.
169//   - fee: Fee tier identifying the registered pool to swap against.
170//   - recipient: Address that receives the output token transfer.
171//   - zeroForOne: Swap direction; true sells token0 for token1, false sells
172//     token1 for token0.
173//   - amountSpecified: Signed decimal amount; positive values request exact
174//     input, negative values request exact output, and zero is invalid.
175//   - sqrtPriceLimitX96: Decimal Q96 square-root-price limit that bounds the
176//     swap in its direction; an invalid bound causes the call to panic.
177//   - swapCallback: Callback that must settle the input delta after output is
178//     sent; it receives signed int64 token deltas and a callback marker, and a
179//     non-nil error aborts the swap.
180//
181// Returns:
182//   - string: Signed decimal token0 delta from the pool's perspective; a
183//     positive value is owed to the pool and a negative value is sent out.
184//   - string: Signed decimal token1 delta from the pool's perspective; a
185//     positive value is owed to the pool and a negative value is sent out.
186func (i *poolV1) Swap(
187	_ int,
188	rlm realm,
189	token0Path string,
190	token1Path string,
191	fee uint32,
192	recipient address,
193	zeroForOne bool,
194	amountSpecified string,
195	sqrtPriceLimitX96 string,
196	swapCallback func(cur realm, amount0Delta, amount1Delta int64, _ *pl.CallbackMarker) error,
197) (string, string) {
198	access.AssertIsRlmCurrent(0, rlm)
199
200	i.assertPoolUnlocked()
201	halt.AssertIsNotHaltedPool()
202
203	assertIsNotUserCall(0, rlm)
204	assertIsValidTokenOrder(token0Path, token1Path)
205
206	amounts := i256.MustFromDecimal(amountSpecified)
207	if amounts.IsZero() {
208		panic(newErrorWithDetail(
209			errInvalidSwapAmount,
210			"amountSpecified == 0",
211		))
212	}
213
214	pool := i.mustGetPoolBy(token0Path, token1Path, fee)
215	observations := i.mustGetObservations(pool.PoolPath())
216
217	slot0Start := pool.Slot0()
218	i.lockPool(0, rlm)
219	defer i.unlockPool(0, rlm)
220
221	blockTimestamp := time.Now().Unix()
222
223	// Call swap start hook if set
224	if i.store.HasSwapStartHook() {
225		swapStartHook := i.store.GetSwapStartHook()
226
227		if swapStartHook != nil {
228			swapStartHook(cross(rlm), pool.PoolPath(), blockTimestamp)
229		}
230	}
231
232	sqrtPriceLimit := u256.MustFromDecimal(sqrtPriceLimitX96)
233	validatePriceLimits(slot0Start, zeroForOne, sqrtPriceLimit)
234
235	feeGrowthGlobalX128 := getFeeGrowthGlobal(pool, zeroForOne)
236	feeProtocol := getFeeProtocol(slot0Start, zeroForOne)
237	cache := newSwapCache(feeProtocol, pool.Liquidity().Clone(), blockTimestamp, slot0Start)
238	state := newSwapState(amounts, feeGrowthGlobalX128, cache.liquidityStart.Clone(), slot0Start)
239
240	comp := SwapComputation{
241		AmountSpecified:   amounts,
242		SqrtPriceLimitX96: sqrtPriceLimit,
243		ZeroForOne:        zeroForOne,
244		ExactInput:        amounts.Gt(zeroI256),
245		InitialState:      state,
246		Cache:             cache,
247	}
248
249	var hook func(cur realm, poolPath string, tickId int32, zeroForOne bool, timestamp int64)
250	if i.store.HasTickCrossHook() {
251		hook = i.store.GetTickCrossHook()
252	}
253	onTickCross := func(pool *pl.Pool, tickId int32, zeroForOne bool, timestamp int64) {
254		chain.Emit(
255			"PoolTickCross",
256			"poolPath", pool.PoolPath(),
257			"tick", NewTickEventInfo(tickId, *mustGetTick(pool, tickId)).ToString(),
258		)
259		if hook != nil {
260			hook(cross(rlm), pool.PoolPath(), tickId, zeroForOne, timestamp)
261		}
262	}
263
264	result, err := i.computeSwap(pool, observations, comp, onTickCross)
265	if err != nil {
266		panic(err)
267	}
268
269	// Update oracle BEFORE applying swap result (using pre-swap state)
270	if result.NewTick != slot0Start.Tick() {
271		observationIndex, observationCardinality, err := writeObservation(
272			observations,
273			slot0Start.ObservationIndex(),
274			cache.blockTimestamp,
275			slot0Start.Tick(),
276			cache.liquidityStart,
277			slot0Start.ObservationCardinality(),
278			slot0Start.ObservationCardinalityNext(),
279		)
280		if err != nil {
281			panic(err)
282		}
283		slot0 := pool.Slot0()
284		slot0.SetObservationIndex(observationIndex)
285		slot0.SetObservationCardinality(observationCardinality)
286		pool.SetSlot0(slot0)
287	}
288
289	applySwapResult(pool, result)
290
291	if i.store.HasSwapEndHook() {
292		swapEndHook := i.store.GetSwapEndHook()
293		if swapEndHook != nil {
294			err := swapEndHook(cross(rlm), pool.PoolPath())
295			if err != nil {
296				panic(err)
297			}
298		}
299	}
300
301	// transfer swap result to recipient then receive input tokens from swap callback
302	if zeroForOne {
303		// receive token0 from swap callback
304		// send token1 to recipient (output)
305		if result.Amount1.IsNeg() {
306			i.safeTransfer(0, rlm, pool, recipient, token1Path, result.Amount1.Abs(), false)
307		}
308		i.safeSwapCallback(0, rlm, pool, token0Path, result.Amount0, result.Amount1, zeroForOne, swapCallback)
309	} else {
310		// receive token1 from swap callback
311		// send token0 to recipient (output)
312		if result.Amount0.IsNeg() {
313			i.safeTransfer(0, rlm, pool, recipient, token0Path, result.Amount0.Abs(), true)
314		}
315		i.safeSwapCallback(0, rlm, pool, token1Path, result.Amount1, result.Amount0, zeroForOne, swapCallback)
316	}
317
318	slot0 := pool.Slot0()
319	lastObservation, err := observationAt(observations, slot0.ObservationIndex())
320	if err != nil {
321		panic(err)
322	}
323
324	token0Amount := result.Amount0.ToString()
325	token1Amount := result.Amount1.ToString()
326
327	previousRealm := rlm.Previous()
328
329	chain.Emit(
330		"Swap",
331		"prevAddr", previousRealm.Address().String(),
332		"prevRealm", previousRealm.PkgPath(),
333		"poolPath", pool.PoolPath(),
334		"zeroForOne", utils.FormatBool(zeroForOne),
335		"requestAmount", amountSpecified,
336		"sqrtPriceLimitX96", sqrtPriceLimitX96,
337		"recipient", recipient.String(),
338		"token0Amount", token0Amount,
339		"token1Amount", token1Amount,
340		"protocolFee0", utils.FormatInt(pool.ProtocolFeesToken0()),
341		"protocolFee1", utils.FormatInt(pool.ProtocolFeesToken1()),
342		"sqrtPriceX96", pool.Slot0SqrtPriceX96().ToString(),
343		"exactIn", strconv.FormatBool(comp.ExactInput),
344		"currentTick", strconv.FormatInt(int64(pool.Slot0Tick()), 10),
345		"liquidity", pool.Liquidity().ToString(),
346		"feeGrowthGlobal0X128", pool.FeeGrowthGlobal0X128().ToString(),
347		"feeGrowthGlobal1X128", pool.FeeGrowthGlobal1X128().ToString(),
348		"balanceToken0", utils.FormatInt(pool.BalanceToken0()),
349		"balanceToken1", utils.FormatInt(pool.BalanceToken1()),
350		"tickCumulative", utils.FormatInt(lastObservation.TickCumulative()),
351		"secondsPerLiquidityCumulativeX128", lastObservation.SecondsPerLiquidityCumulativeX128(),
352		"observationTimestamp", utils.FormatInt(lastObservation.BlockTimestamp()),
353	)
354
355	return token0Amount, token1Amount
356}
357
358// DrySwap simulates a swap against an isolated pool and observation snapshot;
359// it never writes stored pool or oracle state.
360//
361// Parameters:
362//   - token0Path: Registered path of token0; it must be the first token in the
363//     pool's canonical token ordering.
364//   - token1Path: Registered path of token1; it must be the second token in
365//     the pool's canonical token ordering.
366//   - fee: Fee tier identifying the registered pool to simulate.
367//   - zeroForOne: Swap direction; true sells token0 for token1, false sells
368//     token1 for token0.
369//   - amountSpecified: Signed decimal amount; positive values request exact
370//     input, negative values request exact output, and zero is invalid.
371//   - sqrtPriceLimitX96: Decimal Q96 square-root-price limit that bounds the
372//     simulation in its direction.
373//
374// Returns:
375//   - amount0: Signed decimal token0 delta from the pool's perspective, or
376//     "0" when simulation fails.
377//   - amount1: Signed decimal token1 delta from the pool's perspective, or
378//     "0" when simulation fails.
379//   - err: Nil when the simulation succeeds; non-nil for invalid inputs,
380//     missing pool state, invalid limits, computation failures, or insufficient
381//     pool output balance.
382func (i *poolV1) DrySwap(
383	token0Path string,
384	token1Path string,
385	fee uint32,
386	zeroForOne bool,
387	amountSpecified string,
388	sqrtPriceLimitX96 string,
389) (amount0, amount1 string, err error) {
390	defer func() {
391		if r := recover(); r != nil {
392			amount0 = "0"
393			amount1 = "0"
394			if recoveredErr, ok := r.(error); ok {
395				err = recoveredErr
396			} else {
397				err = ufmt.Errorf("%v", r)
398			}
399		}
400	}()
401
402	i.assertPoolUnlocked()
403
404	amounts := i256.MustFromDecimal(amountSpecified)
405	if amounts.IsZero() {
406		return "0", "0", errors.New(errInvalidSwapAmount)
407	}
408
409	pool := i.mustGetPoolBy(token0Path, token1Path, fee)
410	blockTimestamp := time.Now().Unix()
411
412	slot0Start := pool.Slot0()
413	sqrtPriceLimit := u256.MustFromDecimal(sqrtPriceLimitX96)
414	validatePriceLimits(slot0Start, zeroForOne, sqrtPriceLimit)
415
416	feeGrowthGlobalX128 := getFeeGrowthGlobal(pool, zeroForOne)
417	feeProtocol := getFeeProtocol(slot0Start, zeroForOne)
418	cache := newSwapCache(feeProtocol, pool.Liquidity().Clone(), blockTimestamp, slot0Start)
419	cache.readOnly = true
420	state := newSwapState(amounts, feeGrowthGlobalX128, cache.liquidityStart, slot0Start)
421
422	comp := SwapComputation{
423		AmountSpecified:   amounts,
424		SqrtPriceLimitX96: sqrtPriceLimit,
425		ZeroForOne:        zeroForOne,
426		ExactInput:        amounts.Gt(zeroI256),
427		InitialState:      state,
428		Cache:             cache,
429	}
430
431	result, err := i.computeSwap(pool, nil, comp, nil)
432	if err != nil {
433		return "0", "0", err
434	}
435
436	if zeroForOne {
437		if pool.BalanceToken1() < gnsmath.SafeConvertToInt64(result.Amount1.Abs()) {
438			return "0", "0", errors.New(errInsufficientPoolBalance)
439		}
440	} else {
441		if pool.BalanceToken0() < gnsmath.SafeConvertToInt64(result.Amount0.Abs()) {
442			return "0", "0", errors.New(errInsufficientPoolBalance)
443		}
444	}
445
446	return result.Amount0.ToString(), result.Amount1.ToString(), nil
447}
448
449// tickCrossHookFn is invoked after an initialized tick is crossed. Swap uses it
450// for externally visible side effects; DrySwap passes nil.
451type tickCrossHookFn func(pool *pl.Pool, tickId int32, zeroForOne bool, timestamp int64)
452
453// computeSwap calculates swap amounts; tick accounting is written unless Cache.readOnly is set.
454// The computation continues until either:
455// - The entire amount is consumed (amountSpecifiedRemaining = 0)
456// - The price limit is reached (sqrtPriceX96 = sqrtPriceLimitX96)
457//
458// Important: This function is critical for AMM price discovery. It iterates through
459// tick ranges, calculating swap amounts and fees for each liquidity segment.
460// Returns an error if the computation fails at any step.
461//
462// The optional `onTickCross` callback is invoked when an initialized tick is
463// crossed; Swap supplies a hook that performs a cross-realm call into the
464// configured tick-cross hook, while DrySwap passes nil.
465func (i *poolV1) computeSwap(pool *pl.Pool, observations *pl.ObservationTree, comp SwapComputation, onTickCross tickCrossHookFn) (*SwapResult, error) {
466	state := comp.InitialState
467	var err error
468
469	// Compute swap steps until completion
470	for shouldContinueSwap(state, comp.SqrtPriceLimitX96) {
471		state, err = i.computeSwapStep(state, pool, observations, comp.ZeroForOne, comp.SqrtPriceLimitX96, comp.ExactInput, comp.Cache, onTickCross)
472		if err != nil {
473			return nil, err
474		}
475	}
476
477	// Calculate final amounts
478	amount0 := state.amountCalculated
479	amount1 := i256.Zero().Sub(comp.AmountSpecified, state.amountSpecifiedRemaining)
480	if comp.ZeroForOne == comp.ExactInput {
481		amount0, amount1 = amount1, amount0
482	}
483
484	// Prepare result
485	result := &SwapResult{
486		Amount0:              amount0,
487		Amount1:              amount1,
488		NewSqrtPrice:         state.sqrtPriceX96,
489		NewTick:              state.tick,
490		NewLiquidity:         state.liquidity,
491		NewProtocolFeeToken0: pool.ProtocolFeesToken0(),
492		NewProtocolFeeToken1: pool.ProtocolFeesToken1(),
493		FeeGrowthGlobal0X128: pool.FeeGrowthGlobal0X128(),
494		FeeGrowthGlobal1X128: pool.FeeGrowthGlobal1X128(),
495	}
496
497	// Update protocol fees if necessary
498	if comp.ZeroForOne {
499		if state.protocolFee.Gt(zero) {
500			result.NewProtocolFeeToken0 = gnsmath.SafeAddInt64(result.NewProtocolFeeToken0, gnsmath.SafeConvertToInt64(state.protocolFee))
501		}
502		result.FeeGrowthGlobal0X128 = state.feeGrowthGlobalX128.Clone()
503	} else {
504		if state.protocolFee.Gt(zero) {
505			result.NewProtocolFeeToken1 = gnsmath.SafeAddInt64(result.NewProtocolFeeToken1, gnsmath.SafeConvertToInt64(state.protocolFee))
506		}
507		result.FeeGrowthGlobal1X128 = state.feeGrowthGlobalX128.Clone()
508	}
509
510	return result, nil
511}
512
513// applySwapResult updates pool state with computed results.
514// All state changes are applied at once to maintain consistency
515func applySwapResult(pool *pl.Pool, result *SwapResult) {
516	slot0 := pool.Slot0()
517	slot0.SetSqrtPriceX96(result.NewSqrtPrice)
518	slot0.SetTick(result.NewTick)
519	pool.SetSlot0(slot0)
520
521	pool.SetLiquidity(result.NewLiquidity)
522	pool.SetProtocolFeesToken0(result.NewProtocolFeeToken0)
523	pool.SetProtocolFeesToken1(result.NewProtocolFeeToken1)
524	pool.SetFeeGrowthGlobal0X128(result.FeeGrowthGlobal0X128)
525	pool.SetFeeGrowthGlobal1X128(result.FeeGrowthGlobal1X128)
526}
527
528// validatePriceLimits ensures the provided price limit is valid for the swap direction
529// The function enforces that:
530// For zeroForOne (selling token0):
531//   - Price limit must be below current price
532//   - Price limit must be above MIN_SQRT_RATIO
533//
534// For !zeroForOne (selling token1):
535//   - Price limit must be above current price
536//   - Price limit must be below MAX_SQRT_RATIO
537func validatePriceLimits(slot0 pl.Slot0, zeroForOne bool, sqrtPriceLimitX96 *u256.Uint) {
538	if zeroForOne {
539		cond1 := sqrtPriceLimitX96.Lt(slot0.SqrtPriceX96())
540		cond2 := sqrtPriceLimitX96.Gt(consts.MinSqrtRatio())
541		if !(cond1 && cond2) {
542			panic(newErrorWithDetail(
543				errPriceOutOfRange,
544				ufmt.Sprintf("sqrtPriceLimitX96(%s) < slot0Start.sqrtPriceX96(%s) && sqrtPriceLimitX96(%s) > MIN_SQRT_RATIO(%s)",
545					sqrtPriceLimitX96.ToString(),
546					slot0.SqrtPriceX96().ToString(),
547					sqrtPriceLimitX96.ToString(),
548					MIN_SQRT_RATIO),
549			))
550		}
551	} else {
552		cond1 := sqrtPriceLimitX96.Gt(slot0.SqrtPriceX96())
553		cond2 := sqrtPriceLimitX96.Lt(consts.MaxSqrtRatio())
554		if !(cond1 && cond2) {
555			panic(newErrorWithDetail(
556				errPriceOutOfRange,
557				ufmt.Sprintf("sqrtPriceLimitX96(%s) > slot0Start.sqrtPriceX96(%s) && sqrtPriceLimitX96(%s) < MAX_SQRT_RATIO(%s)",
558					sqrtPriceLimitX96.ToString(),
559					slot0.SqrtPriceX96().ToString(),
560					sqrtPriceLimitX96.ToString(),
561					MAX_SQRT_RATIO),
562			))
563		}
564	}
565}
566
567// getFeeProtocol returns the appropriate fee protocol based on zero for one.
568// When zeroForOne is true, we want the lower 4 bits (% 16).
569// Otherwise, we want the upper 4 bits (/ 16).
570func getFeeProtocol(slot0 pl.Slot0, zeroForOne bool) uint8 {
571	shift := uint8(0)
572	if !zeroForOne {
573		shift = 4
574	}
575	return (slot0.FeeProtocol() >> shift) & uint8(0xF)
576}
577
578// getFeeGrowthGlobal returns the appropriate fee growth global based on zero for one.
579func getFeeGrowthGlobal(pool *pl.Pool, zeroForOne bool) *u256.Uint {
580	if zeroForOne {
581		return pool.FeeGrowthGlobal0X128().Clone()
582	}
583	return pool.FeeGrowthGlobal1X128().Clone()
584}
585
586// shouldContinueSwap checks if swap should continue based on remaining amount and price limit.
587func shouldContinueSwap(state SwapState, sqrtPriceLimitX96 *u256.Uint) bool {
588	return !state.amountSpecifiedRemaining.IsZero() && !state.sqrtPriceX96.Eq(sqrtPriceLimitX96)
589}
590
591// computeSwapStep executes a single step of swap and returns new state
592func (i *poolV1) computeSwapStep(
593	state SwapState,
594	pool *pl.Pool,
595	observations *pl.ObservationTree,
596	zeroForOne bool,
597	sqrtPriceLimitX96 *u256.Uint,
598	exactInput bool,
599	cache *SwapCache,
600	onTickCross tickCrossHookFn,
601) (SwapState, error) {
602	step := computeSwapStepInit(state, pool, zeroForOne)
603
604	// determining the price target for this step
605	sqrtRatioTargetX96 := computeTargetSqrtRatio(step, sqrtPriceLimitX96, zeroForOne).Clone()
606
607	// computing the amounts to be swapped at this step
608	var (
609		newState SwapState
610		err      error
611	)
612
613	newState, step = computeAmounts(state, sqrtRatioTargetX96, pool, step)
614	newState, err = updateAmounts(step, newState, exactInput)
615	if err != nil {
616		return state, err
617	}
618
619	// if the protocol fee is on, calculate how much is owed,
620	// decrement fee amount, and increment protocol fee
621	if cache.feeProtocol > 0 {
622		newState, step, err = updateFeeProtocol(step, cache.feeProtocol, newState)
623		if err != nil {
624			return state, err
625		}
626	}
627
628	// update global fee tracker
629	if newState.liquidity.Gt(u256.Zero()) {
630		update := u256.MulDiv(step.feeAmount, fixedPointQ128, newState.liquidity)
631		feeGrowthGlobalX128 := u256.Zero().Add(newState.feeGrowthGlobalX128, update)
632		newState.setFeeGrowthGlobalX128(feeGrowthGlobalX128)
633	}
634
635	// handling tick transitions
636	if newState.sqrtPriceX96.Eq(step.sqrtPriceNextX96) {
637		newState, err = i.tickTransition(step, zeroForOne, newState, pool, observations, cache, onTickCross)
638		if err != nil {
639			return state, err
640		}
641	} else if newState.sqrtPriceX96.Neq(step.sqrtPriceStartX96) {
642		newState.setTick(gnsmath.TickMathGetTickAtSqrtRatio(newState.sqrtPriceX96))
643	}
644
645	return newState, nil
646}
647
648// updateFeeProtocol calculates and updates protocol fees for the current step.
649func updateFeeProtocol(step StepComputations, feeProtocol uint8, state SwapState) (SwapState, StepComputations, error) {
650	delta := u256.Zero().Div(step.feeAmount, u256.NewUint(uint64(feeProtocol)))
651
652	newFeeAmount, overflow := u256.Zero().SubOverflow(step.feeAmount, delta)
653	if overflow {
654		return state, step, errors.New(errUnderflow)
655	}
656
657	step.feeAmount = newFeeAmount
658
659	newProtocolFee, overflow := u256.Zero().AddOverflow(state.protocolFee, delta)
660	if overflow {
661		return state, step, errors.New(errOverflow)
662	}
663	state.protocolFee = newProtocolFee
664
665	return state, step, nil
666}
667
668// computeSwapStepInit initializes the computation for a single swap step.
669func computeSwapStepInit(state SwapState, pool *pl.Pool, zeroForOne bool) StepComputations {
670	var step StepComputations
671	step.sqrtPriceStartX96 = state.sqrtPriceX96
672	tickNext, initialized := tickBitmapNextInitializedTickWithInOneWord(
673		pool,
674		state.tick,
675		pool.TickSpacing(),
676		zeroForOne,
677	)
678
679	step.tickNext = tickNext
680	step.initialized = initialized
681
682	// prevent overshoot the min/max tick
683	step.clampTickNext()
684	// get the price for the next tick
685	step.sqrtPriceNextX96 = gnsmath.TickMathGetSqrtRatioAtTick(step.tickNext)
686	return step
687}
688
689// computeTargetSqrtRatio determines the target sqrt price for the current swap step.
690func computeTargetSqrtRatio(step StepComputations, sqrtPriceLimitX96 *u256.Uint, zeroForOne bool) *u256.Uint {
691	if shouldUsePriceLimit(step.sqrtPriceNextX96, sqrtPriceLimitX96, zeroForOne) {
692		return sqrtPriceLimitX96
693	}
694	return step.sqrtPriceNextX96
695}
696
697// shouldUsePriceLimit returns true if the price limit should be used instead of the next tick price
698func shouldUsePriceLimit(sqrtPriceNext, sqrtPriceLimit *u256.Uint, zeroForOne bool) bool {
699	if zeroForOne {
700		return sqrtPriceNext.Lt(sqrtPriceLimit)
701	}
702	return sqrtPriceNext.Gt(sqrtPriceLimit)
703}
704
705// computeAmounts calculates the input and output amounts for the current swap step.
706func computeAmounts(state SwapState, sqrtRatioTargetX96 *u256.Uint, pool *pl.Pool, step StepComputations) (SwapState, StepComputations) {
707	sqrtPriceX96, amountIn, amountOut, feeAmount := gnsmath.SwapMathComputeSwapStep(
708		state.sqrtPriceX96,
709		sqrtRatioTargetX96,
710		state.liquidity,
711		state.amountSpecifiedRemaining,
712		uint64(pool.Fee()),
713	)
714
715	step.amountIn = amountIn
716	step.amountOut = amountOut
717	step.feeAmount = feeAmount
718
719	state.setSqrtPriceX96(sqrtPriceX96)
720
721	return state, step
722}
723
724// updateAmounts calculates new remaining and calculated amounts based on the swap step.
725// For exact input swaps:
726//   - Decrements remaining input amount by (amountIn + feeAmount)
727//   - Decrements calculated amount by amountOut
728//
729// For exact output swaps:
730//   - Increments remaining output amount by amountOut
731//   - Increments calculated amount by (amountIn + feeAmount)
732func updateAmounts(step StepComputations, state SwapState, exactInput bool) (SwapState, error) {
733	amountInWithFeeU256 := u256.Zero().Add(step.amountIn, step.feeAmount)
734	if amountInWithFeeU256.Gt(maxInt256) {
735		return state, errors.New(errOverflow)
736	}
737
738	amountInWithFee := i256.FromUint256(amountInWithFeeU256)
739	if step.amountOut.Gt(maxInt256) {
740		return state, errors.New(errOverflow)
741	}
742
743	var (
744		amountSpecifiedRemaining *i256.Int
745		amountCalculated         *i256.Int
746		overflow                 bool
747	)
748
749	if exactInput {
750		amountSpecifiedRemaining, overflow = i256.Zero().SubOverflow(state.amountSpecifiedRemaining, amountInWithFee)
751		if overflow {
752			return state, errors.New(errUnderflow)
753		}
754		amountCalculated, overflow = i256.Zero().SubOverflow(state.amountCalculated, i256.FromUint256(step.amountOut))
755		if overflow {
756			return state, errors.New(errUnderflow)
757		}
758	} else {
759		amountSpecifiedRemaining, overflow = i256.Zero().AddOverflow(state.amountSpecifiedRemaining, i256.FromUint256(step.amountOut))
760		if overflow {
761			return state, errors.New(errOverflow)
762		}
763		amountCalculated, overflow = i256.Zero().AddOverflow(state.amountCalculated, amountInWithFee)
764		if overflow {
765			return state, errors.New(errOverflow)
766		}
767	}
768
769	// If an overflowed value is stored in state, it may cause problems in the next step
770	if amountCalculated.Gt(maxInt64) || amountSpecifiedRemaining.Gt(maxInt64) {
771		return state, errors.New(errOverflow)
772	}
773
774	// If an underflowed value is stored in state, it may cause problems in the next step
775	if amountCalculated.Lt(minInt64) || amountSpecifiedRemaining.Lt(minInt64) {
776		return state, errors.New(errUnderflow)
777	}
778
779	state.amountSpecifiedRemaining = amountSpecifiedRemaining
780	state.amountCalculated = amountCalculated
781
782	return state, nil
783}
784
785// tickTransition handles the transition between price ticks during a swap
786func (i *poolV1) tickTransition(step StepComputations, zeroForOne bool, state SwapState, pool *pl.Pool, observations *pl.ObservationTree, cache *SwapCache, onTickCross tickCrossHookFn) (SwapState, error) {
787	// ensure existing state to keep immutability
788	newState := state
789
790	if step.initialized {
791		var liquidityNet *i256.Int
792		if cache.readOnly {
793			// Quotes need only the liquidity delta. Outside fee/oracle accumulators
794			// do not affect pricing, and the monotonic path never revisits a tick.
795			tick := getTick(pool, step.tickNext)
796			liquidityNet = i256.MustFromDecimal(tick.LiquidityNet())
797		} else {
798			// Compute oracle values on first initialized tick cross.
799			if !cache.computedLatestObservation {
800				tickCumulative, secondsPerLiquidityStr, err := observeSingle(
801					observations,
802					cache.blockTimestamp,
803					0,
804					cache.slot0Start.Tick(),
805					cache.slot0Start.ObservationIndex(),
806					cache.liquidityStart,
807					cache.slot0Start.ObservationCardinality(),
808				)
809				if err != nil {
810					return newState, err
811				}
812
813				cache.tickCumulative = tickCumulative
814				cache.secondsPerLiquidityCumulativeX128 = u256.MustFromDecimal(secondsPerLiquidityStr)
815				cache.computedLatestObservation = true
816			}
817
818			fee0, fee1 := u256.Zero(), u256.Zero()
819			if zeroForOne {
820				fee0 = state.feeGrowthGlobalX128
821				fee1 = pool.FeeGrowthGlobal1X128()
822			} else {
823				fee0 = pool.FeeGrowthGlobal0X128()
824				fee1 = state.feeGrowthGlobalX128
825			}
826
827			liquidityNet = tickCross(
828				pool,
829				step.tickNext,
830				fee0,
831				fee1,
832				cache.secondsPerLiquidityCumulativeX128,
833				cache.tickCumulative,
834				cache.blockTimestamp,
835			)
836		}
837
838		if zeroForOne {
839			liquidityNet = i256.Zero().Neg(liquidityNet)
840		}
841
842		newState.liquidity = gnsmath.LiquidityMathAddDelta(state.liquidity, liquidityNet)
843
844		if !cache.readOnly && onTickCross != nil {
845			onTickCross(pool, step.tickNext, zeroForOne, cache.blockTimestamp)
846		}
847	}
848
849	newState.tick = step.tickNext
850	if zeroForOne {
851		newState.tick = step.tickNext - 1
852	}
853
854	return newState, nil
855}