swap_inner.gno
6.73 Kb · 238 lines
1package v1
2
3import (
4 "gno.land/p/gnoswap/gnsmath"
5 ufmt "gno.land/p/nt/ufmt/v0"
6 "gno.land/r/gnoswap/access"
7 "gno.land/r/gnoswap/router"
8
9 i256 "gno.land/p/gnoswap/int256"
10 prbac "gno.land/p/gnoswap/rbac"
11 u256 "gno.land/p/gnoswap/uint256"
12
13 pl "gno.land/r/gnoswap/pool"
14 plv1 "gno.land/r/gnoswap/pool/v1"
15)
16
17const (
18 MIN_SQRT_RATIO string = "4295128739" // same as TickMathGetSqrtRatioAtTick(MIN_TICK)
19 MAX_SQRT_RATIO string = "1461446703485210103287273052203988822378723970342" // same as TickMathGetSqrtRatioAtTick(MAX_TICK)
20)
21
22// Precomputed sqrt price limits per fee tier and direction.
23// TickMathGetSqrtRatioAtTick costs ~2M-2.4M gas per call and only depends on fee tier,
24// so we compute all 8 values (4 fees × 2 directions) once at init and cache them.
25// Callers use the cached pointer read-only (only .ToString() is called on the result).
26var (
27 sqrtPriceLimitForward = make(map[uint32]*u256.Uint) // zeroForOne=true: sqrtRatioAtTick(minTick+1) + 1
28 sqrtPriceLimitBackward = make(map[uint32]*u256.Uint) // zeroForOne=false: sqrtRatioAtTick(maxTick-1) - 1
29)
30
31func init() {
32 for _, fee := range []uint32{plv1.FeeTier100, plv1.FeeTier500, plv1.FeeTier3000, plv1.FeeTier10000} {
33 // zeroForOne=true: price must stay above minimum
34 minTick := getMinTick(fee) + 1
35 fwd := gnsmath.TickMathGetSqrtRatioAtTick(minTick) // returns a fresh allocation
36 if fwd.IsZero() {
37 fwd = u256.MustFromDecimal(MIN_SQRT_RATIO)
38 }
39 fwd.Add(fwd, one) // fwd += 1 in-place
40 sqrtPriceLimitForward[fee] = fwd
41
42 // zeroForOne=false: price must stay below maximum
43 maxTick := getMaxTick(fee) - 1
44 bwd := gnsmath.TickMathGetSqrtRatioAtTick(maxTick) // returns a fresh allocation
45 if bwd.IsZero() {
46 bwd = u256.MustFromDecimal(MAX_SQRT_RATIO)
47 }
48 bwd.Sub(bwd, one) // bwd -= 1 in-place
49 sqrtPriceLimitBackward[fee] = bwd
50 }
51}
52
53// swapInner executes the core swap logic by interacting with the pool contract.
54// Returns poolRecv (tokens received by pool) and poolOut (tokens sent by pool).
55func (r *routerV1) swapInner(
56 _ int,
57 rlm realm,
58 amountSpecified int64,
59 recipient address,
60 sqrtPriceLimitX96 *u256.Uint,
61 data SwapCallbackData,
62) (int64, int64) {
63 token0Path, token1Path := data.tokenIn, data.tokenOut
64 zeroForOne := data.tokenIn < data.tokenOut
65
66 if !zeroForOne {
67 token0Path, token1Path = token1Path, token0Path
68 }
69
70 sqrtPriceLimitX96 = calculateSqrtPriceLimitForSwap(zeroForOne, data.fee, sqrtPriceLimitX96)
71
72 amount0Str, amount1Str := pl.Swap(
73 cross(rlm),
74 token0Path,
75 token1Path,
76 data.fee,
77 recipient,
78 zeroForOne,
79 formatInt64(amountSpecified),
80 sqrtPriceLimitX96.ToString(),
81 data.payer,
82 func(cur realm, amount0Delta, amount1Delta int64, _ *pl.CallbackMarker) error {
83 // assert is pool to prevent unauthorized callback
84 caller := cur.Previous().Address()
85 access.AssertIsPool(caller)
86
87 return router.SwapCallback(cross(cur), token0Path, token1Path, amount0Delta, amount1Delta, data.payer)
88 },
89 )
90
91 amount0 := i256.MustFromDecimal(amount0Str)
92 amount1 := i256.MustFromDecimal(amount1Str)
93
94 poolOut, poolRecv := i256MinMax(amount0, amount1)
95 if poolRecv.IsOverflow() || poolOut.IsOverflow() {
96 panic("overflow in swapInner")
97 }
98
99 return poolRecv.Int64(), poolOut.Int64()
100}
101
102// swapDryInner performs a dry-run of a swap operation without executing it.
103func (r *routerV1) swapDryInner(
104 amountSpecified int64,
105 sqrtPriceLimitX96 *u256.Uint,
106 data SwapCallbackData,
107) (int64, int64) {
108 zeroForOne := data.tokenIn < data.tokenOut
109 sqrtPriceLimitX96 = calculateSqrtPriceLimitForSwap(zeroForOne, data.fee, sqrtPriceLimitX96)
110
111 // check possible
112 amount0Str, amount1Str, ok := pl.DrySwap(
113 data.tokenIn,
114 data.tokenOut,
115 data.fee,
116 zeroForOne,
117 formatInt64(amountSpecified),
118 sqrtPriceLimitX96.ToString(),
119 )
120 if !ok {
121 return 0, 0
122 }
123
124 amount0 := i256.MustFromDecimal(amount0Str)
125 amount1 := i256.MustFromDecimal(amount1Str)
126
127 poolOut, poolRecv := i256MinMax(amount0, amount1)
128 if poolRecv.IsOverflow() || poolOut.IsOverflow() {
129 panic("overflow in swapDryInner")
130 }
131
132 return poolRecv.Int64(), poolOut.Int64()
133}
134
135// RealSwapExecutor implements SwapExecutor for actual swaps.
136type RealSwapExecutor struct {
137 rlm realm
138 router *routerV1
139}
140
141// execute performs the actual swap execution.
142func (e *RealSwapExecutor) execute(p *SingleSwapParams) (int64, int64) {
143 caller := e.rlm.Previous().Address()
144 recipient := access.MustGetAddress(prbac.ROLE_ROUTER.String())
145
146 return e.router.swapInner(
147 0,
148 e.rlm,
149 p.amountSpecified,
150 recipient, // if single swap => user will receive
151 p.SqrtPriceLimitX96(), // sqrtPriceLimitX96
152 newSwapCallbackData(p, caller),
153 )
154}
155
156// DrySwapExecutor implements SwapExecutor for dry swaps.
157type DrySwapExecutor struct {
158 router *routerV1
159 // payer is the user's address resolved at the entry point (DrySwapRoute),
160 // since dry-run paths do not have a realm value to read PreviousRealm from.
161 payer address
162}
163
164// execute performs the dry swap execution.
165func (e *DrySwapExecutor) execute(p *SingleSwapParams) (int64, int64) {
166 return e.router.swapDryInner(
167 p.amountSpecified,
168 zero,
169 newSwapCallbackData(p, e.payer),
170 )
171}
172
173// calculateSqrtPriceLimitForSwap returns the price limit for a swap operation.
174// If a non-zero limit is provided by the caller, it is returned as-is.
175// Otherwise, returns the precomputed limit for the given fee tier and direction.
176// The returned pointer is shared (read-only — callers must not mutate it).
177func calculateSqrtPriceLimitForSwap(zeroForOne bool, fee uint32, sqrtPriceLimitX96 *u256.Uint) *u256.Uint {
178 if !sqrtPriceLimitX96.IsZero() {
179 return sqrtPriceLimitX96
180 }
181
182 if zeroForOne {
183 return mustGetSqrtPriceLimit(sqrtPriceLimitForward, fee)
184 }
185 return mustGetSqrtPriceLimit(sqrtPriceLimitBackward, fee)
186}
187
188func mustGetSqrtPriceLimit(limits map[uint32]*u256.Uint, fee uint32) *u256.Uint {
189 limit, ok := limits[fee]
190 if !ok {
191 panic(addDetailToError(
192 errInvalidPoolFeeTier,
193 ufmt.Sprintf("unknown fee(%d)", fee),
194 ))
195 }
196 return limit
197}
198
199// getMinTick returns the minimum tick value for a given fee tier.
200// The implementation follows Uniswap V3's tick spacing rules where
201// lower fee tiers allow for finer price granularity.
202func getMinTick(fee uint32) int32 {
203 switch fee {
204 case 100:
205 return -887272
206 case 500:
207 return -887270
208 case 3000:
209 return -887220
210 case 10000:
211 return -887200
212 default:
213 panic(addDetailToError(
214 errInvalidPoolFeeTier,
215 ufmt.Sprintf("unknown fee(%d)", fee),
216 ))
217 }
218}
219
220// getMaxTick returns the maximum tick value for a given fee tier.
221// The max tick values are the exact negatives of min tick values.
222func getMaxTick(fee uint32) int32 {
223 switch fee {
224 case 100:
225 return 887272
226 case 500:
227 return 887270
228 case 3000:
229 return 887220
230 case 10000:
231 return 887200
232 default:
233 panic(addDetailToError(
234 errInvalidPoolFeeTier,
235 ufmt.Sprintf("unknown fee(%d)", fee),
236 ))
237 }
238}