Search Apps Documentation Source Content File Folder Download Copy Actions Download

swap_math.gno

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