swap_inner.gno
6.78 Kb · 238 lines
1package router
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 "gno.land/p/gnoswap/utils"
13
14 pl "gno.land/r/gnoswap/pool"
15 plv1 "gno.land/r/gnoswap/pool/v1"
16)
17
18const (
19 MIN_SQRT_RATIO string = "4295128739" // same as TickMathGetSqrtRatioAtTick(MIN_TICK)
20 MAX_SQRT_RATIO string = "1461446703485210103287273052203988822378723970342" // same as TickMathGetSqrtRatioAtTick(MAX_TICK)
21)
22
23// Precomputed sqrt price limits per fee tier and direction.
24// TickMathGetSqrtRatioAtTick costs ~2M-2.4M gas per call and only depends on fee tier,
25// so we compute all 8 values (4 fees × 2 directions) once at init and cache them.
26// Callers use the cached pointer read-only (only .ToString() is called on the result).
27var (
28 sqrtPriceLimitForward = make(map[uint32]*u256.Uint) // zeroForOne=true: sqrtRatioAtTick(minTick+1) + 1
29 sqrtPriceLimitBackward = make(map[uint32]*u256.Uint) // zeroForOne=false: sqrtRatioAtTick(maxTick-1) - 1
30)
31
32func init() {
33 for _, fee := range []uint32{plv1.FeeTier100, plv1.FeeTier500, plv1.FeeTier3000, plv1.FeeTier10000} {
34 // zeroForOne=true: price must stay above minimum
35 minTick := getMinTick(fee) + 1
36 fwd := gnsmath.TickMathGetSqrtRatioAtTick(minTick) // returns a fresh allocation
37 if fwd.IsZero() {
38 fwd = u256.MustFromDecimal(MIN_SQRT_RATIO)
39 }
40 fwd.Add(fwd, u256.One()) // fwd += 1 in-place
41 sqrtPriceLimitForward[fee] = fwd
42
43 // zeroForOne=false: price must stay below maximum
44 maxTick := getMaxTick(fee) - 1
45 bwd := gnsmath.TickMathGetSqrtRatioAtTick(maxTick) // returns a fresh allocation
46 if bwd.IsZero() {
47 bwd = u256.MustFromDecimal(MAX_SQRT_RATIO)
48 }
49 bwd.Sub(bwd, u256.One()) // bwd -= 1 in-place
50 sqrtPriceLimitBackward[fee] = bwd
51 }
52}
53
54// swapInner executes the core swap logic by interacting with the pool contract.
55// Returns poolRecv (tokens received by pool) and poolOut (tokens sent by pool).
56func (r *routerV1) swapInner(
57 _ int,
58 rlm realm,
59 amountSpecified int64,
60 recipient address,
61 sqrtPriceLimitX96 *u256.Uint,
62 data SwapCallbackData,
63) (int64, int64) {
64 token0Path, token1Path := data.tokenIn, data.tokenOut
65 zeroForOne := data.tokenIn < data.tokenOut
66
67 if !zeroForOne {
68 token0Path, token1Path = token1Path, token0Path
69 }
70
71 sqrtPriceLimitX96 = calculateSqrtPriceLimitForSwap(zeroForOne, data.fee, sqrtPriceLimitX96)
72
73 amount0Str, amount1Str := pl.Swap(
74 cross(rlm),
75 token0Path,
76 token1Path,
77 data.fee,
78 recipient,
79 zeroForOne,
80 utils.FormatInt(amountSpecified),
81 sqrtPriceLimitX96.ToString(),
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 utils.FormatInt(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 u256.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}