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}