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_math.gno

7.70 Kb · 243 lines
  1package gnsmath
  2
  3import (
  4	i256 "gno.land/p/gnoswap/int256/v1"
  5	u256 "gno.land/p/gnoswap/uint256/v1"
  6)
  7
  8// denominator represents 100% in the fee calculation basis (1,000,000 = 100%).
  9// Fee calculations use this to convert feePips to actual percentages.
 10// For example, feePips=3000 means 3000/1000000 = 0.3% fee.
 11const denominator = uint64(1_000_000)
 12
 13// SwapMathComputeSwapStep computes one swap step within a single tick range.
 14// It determines the next square-root price, input amount, output amount, and fee,
 15// using exact-input or exact-output semantics from amountRemaining.
 16//
 17// Parameters:
 18//   - sqrtRatioCurrentX96: Current pool square-root price in Q96 fixed-point format.
 19//   - sqrtRatioTargetX96: Tick-boundary square-root price that this step may reach.
 20//   - liquidity: Non-negative liquidity active between the current and target prices.
 21//   - amountRemaining: Signed amount remaining; non-negative selects exact input, negative selects exact output.
 22//   - feePips: Fee rate in millionths of the input amount (3,000 represents 0.3%).
 23//
 24// Returns:
 25//   - sqrtRatioNextX96: Square-root price after applying this step, within the valid pool price bounds.
 26//   - amountIn: Amount of the input token consumed by this step, excluding the fee.
 27//   - amountOut: Amount of the output token produced by this step.
 28//   - feeAmount: Input-token fee charged for this step.
 29//
 30// Panics if an input pointer is nil, feePips is at least 1,000,000, arithmetic
 31// overflows, or the resulting square-root price is outside the valid bounds.
 32func SwapMathComputeSwapStep(
 33	sqrtRatioCurrentX96 *u256.Uint,
 34	sqrtRatioTargetX96 *u256.Uint,
 35	liquidity *u256.Uint,
 36	amountRemaining *i256.Int,
 37	feePips uint64,
 38) (*u256.Uint, *u256.Uint, *u256.Uint, *u256.Uint) {
 39	if sqrtRatioCurrentX96 == nil || sqrtRatioTargetX96 == nil ||
 40		liquidity == nil || amountRemaining == nil {
 41		panic("SwapMathComputeSwapStep: input parameters cannot be nil")
 42	}
 43
 44	// This function is publicly accessible and can be called by external users or contracts.
 45	// While the pool realm only uses predefined fee values (100, 500, 3000, 10000) which are safely within range,
 46	// external callers could potentially pass any feePips value. The fee calculation involves dividing by
 47	// (1000000 - feePips), so feePips must be strictly less than 1000000 to avoid division by zero.
 48	// This follows Uniswap V3's factory-level validation: require(fee < 1000000).
 49	if feePips >= denominator {
 50		panic("SwapMathComputeSwapStep: feePips must be less than 1000000")
 51	}
 52
 53	// zeroForOne determines swap direction based on the relationship of current vs. target
 54	zeroForOne := sqrtRatioCurrentX96.Gte(sqrtRatioTargetX96)
 55
 56	// POSITIVE == EXACT_IN => Estimated AmountOut
 57	// NEGATIVE == EXACT_OUT => Estimated AmountIn
 58	exactIn := !amountRemaining.IsNeg()
 59
 60	amountRemainingAbs := amountRemaining.Abs()
 61	feeRateInPips := u256.NewUint(feePips)
 62	withoutFeeRateInPips := u256.NewUint(denominator - feePips)
 63
 64	sqrtRatioNextX96 := u256.Zero()
 65	amountIn := u256.Zero()
 66	amountOut := u256.Zero()
 67	feeAmount := u256.Zero()
 68
 69	if exactIn {
 70		// Handle EXACT_IN scenario as a separate function
 71		sqrtRatioNextX96, amountIn = handleExactIn(
 72			zeroForOne,
 73			sqrtRatioCurrentX96,
 74			sqrtRatioTargetX96,
 75			liquidity,
 76			amountRemainingAbs, // use absolute value here
 77			withoutFeeRateInPips,
 78		)
 79	} else {
 80		// Handle EXACT_OUT scenario as a separate function
 81		sqrtRatioNextX96, amountOut = handleExactOut(
 82			zeroForOne,
 83			sqrtRatioCurrentX96,
 84			sqrtRatioTargetX96,
 85			liquidity,
 86			amountRemainingAbs,
 87		)
 88	}
 89
 90	// isMax checks if we've hit the boundary price (target)
 91	isMax := sqrtRatioTargetX96.Eq(sqrtRatioNextX96)
 92
 93	// Calculate final amountIn, amountOut if needed
 94	if zeroForOne {
 95		// If isMax && exactIn, we already have the correct amountIn
 96		if !(isMax && exactIn) {
 97			amountIn = getAmount0DeltaHelper(
 98				sqrtRatioNextX96,
 99				sqrtRatioCurrentX96,
100				liquidity,
101				true,
102			)
103		}
104		// If isMax && !exactIn, we already have the correct amountOut
105		if !(isMax && !exactIn) {
106			amountOut = getAmount1DeltaHelper(
107				sqrtRatioNextX96,
108				sqrtRatioCurrentX96,
109				liquidity,
110				false,
111			)
112		}
113	} else {
114		if !(isMax && exactIn) {
115			amountIn = getAmount1DeltaHelper(
116				sqrtRatioCurrentX96,
117				sqrtRatioNextX96,
118				liquidity,
119				true,
120			)
121		}
122		if !(isMax && !exactIn) {
123			amountOut = getAmount0DeltaHelper(
124				sqrtRatioCurrentX96,
125				sqrtRatioNextX96,
126				liquidity,
127				false,
128			)
129		}
130	}
131
132	// If we're in EXACT_OUT mode but overcalculated 'amountOut'
133	if !exactIn && amountOut.Gt(amountRemainingAbs) {
134		amountOut = amountRemainingAbs
135	}
136
137	// Fee logic
138	// If exactIn and we haven't hit the target, the difference is the fee
139	// Else, compute fee from feePips
140	if exactIn && !sqrtRatioNextX96.Eq(sqrtRatioTargetX96) {
141		feeAmount = u256.Zero().Sub(amountRemainingAbs, amountIn)
142	} else {
143		feeAmount = u256.MulDivRoundingUp(
144			amountIn,
145			feeRateInPips,
146			withoutFeeRateInPips,
147		)
148	}
149
150	// Final sanity check for resulting price
151	if sqrtRatioNextX96.Lt(MIN_SQRT_RATIO()) || sqrtRatioNextX96.Gt(MAX_SQRT_RATIO()) {
152		panic(errInvalidPoolSqrtPrice)
153	}
154
155	return sqrtRatioNextX96, amountIn, amountOut, feeAmount
156}
157
158// handleExactIn handles the EXACT_IN scenario for swaps, returning the next sqrt price and
159// a provisional amount. When the target price is reached, it returns the exact amount needed.
160// When the target is not reached, it returns the amount needed to reach the target (which will
161// be recalculated by the caller since we only moved partially).
162// This internal function processes swaps where the input amount is specified exactly.
163func handleExactIn(
164	zeroForOne bool,
165	sqrtRatioCurrentX96,
166	sqrtRatioTargetX96,
167	liquidity,
168	amountRemainingAbs,
169	withoutFeeRateInPips *u256.Uint,
170) (*u256.Uint, *u256.Uint) {
171	amountRemainingLessFee := u256.MulDiv(
172		amountRemainingAbs,
173		withoutFeeRateInPips,
174		u256.NewUint(denominator),
175	)
176
177	amountIn := u256.Zero()
178
179	if zeroForOne {
180		amountIn = getAmount0DeltaHelper(
181			sqrtRatioTargetX96,
182			sqrtRatioCurrentX96,
183			liquidity,
184			true,
185		)
186	} else {
187		amountIn = getAmount1DeltaHelper(
188			sqrtRatioCurrentX96,
189			sqrtRatioTargetX96,
190			liquidity,
191			true,
192		)
193	}
194
195	if amountRemainingLessFee.Gte(amountIn) {
196		return sqrtRatioTargetX96, amountIn
197	}
198
199	// We don't reach target price; use partial move
200	nextSqrt := getNextSqrtPriceFromInput(
201		sqrtRatioCurrentX96,
202		liquidity,
203		amountRemainingLessFee,
204		zeroForOne,
205	)
206
207	// Return the partially moved price and the amount to reach target (will be recalculated by caller)
208	return nextSqrt, amountIn
209}
210
211// handleExactOut handles the EXACT_OUT scenario for swaps, returning the next sqrt price and
212// a provisional amount. When the target price is reached, it returns the exact amount produced.
213// When the target is not reached due to insufficient liquidity, it returns the amount that would
214// be produced if we reached the target (which will be recalculated by the caller).
215// This internal function processes swaps where the output amount is specified exactly.
216func handleExactOut(
217	zeroForOne bool,
218	sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, amountRemainingAbs *u256.Uint,
219) (*u256.Uint, *u256.Uint) {
220	amountOut := u256.Zero()
221
222	if zeroForOne {
223		amountOut = getAmount1DeltaHelper(sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, false)
224	} else {
225		amountOut = getAmount0DeltaHelper(sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, false)
226	}
227
228	// Fast path: if sufficient liquidity, use target price
229	if amountRemainingAbs.Gte(amountOut) {
230		return sqrtRatioTargetX96, amountOut
231	}
232
233	// Otherwise, partial move: compute next price from residual output amount
234	// and return the amount to reach target (will be recalculated by caller)
235	nextSqrt := getNextSqrtPriceFromOutput(
236		sqrtRatioCurrentX96,
237		liquidity,
238		amountRemainingAbs,
239		zeroForOne,
240	)
241
242	return nextSqrt, amountOut
243}