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}