swap_inner.gno
6.79 Kb · 239 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 data.payer,
83 func(cur realm, amount0Delta, amount1Delta int64, _ *pl.CallbackMarker) error {
84 // assert is pool to prevent unauthorized callback
85 caller := cur.Previous().Address()
86 access.AssertIsPool(caller)
87
88 return router.SwapCallback(cross(cur), token0Path, token1Path, amount0Delta, amount1Delta, data.payer)
89 },
90 )
91
92 amount0 := i256.MustFromDecimal(amount0Str)
93 amount1 := i256.MustFromDecimal(amount1Str)
94
95 poolOut, poolRecv := i256MinMax(amount0, amount1)
96 if poolRecv.IsOverflow() || poolOut.IsOverflow() {
97 panic("overflow in swapInner")
98 }
99
100 return poolRecv.Int64(), poolOut.Int64()
101}
102
103// swapDryInner performs a dry-run of a swap operation without executing it.
104func (r *routerV1) swapDryInner(
105 amountSpecified int64,
106 sqrtPriceLimitX96 *u256.Uint,
107 data SwapCallbackData,
108) (int64, int64) {
109 zeroForOne := data.tokenIn < data.tokenOut
110 sqrtPriceLimitX96 = calculateSqrtPriceLimitForSwap(zeroForOne, data.fee, sqrtPriceLimitX96)
111
112 // check possible
113 amount0Str, amount1Str, ok := pl.DrySwap(
114 data.tokenIn,
115 data.tokenOut,
116 data.fee,
117 zeroForOne,
118 utils.FormatInt(amountSpecified),
119 sqrtPriceLimitX96.ToString(),
120 )
121 if !ok {
122 return 0, 0
123 }
124
125 amount0 := i256.MustFromDecimal(amount0Str)
126 amount1 := i256.MustFromDecimal(amount1Str)
127
128 poolOut, poolRecv := i256MinMax(amount0, amount1)
129 if poolRecv.IsOverflow() || poolOut.IsOverflow() {
130 panic("overflow in swapDryInner")
131 }
132
133 return poolRecv.Int64(), poolOut.Int64()
134}
135
136// RealSwapExecutor implements SwapExecutor for actual swaps.
137type RealSwapExecutor struct {
138 rlm realm
139 router *routerV1
140}
141
142// execute performs the actual swap execution.
143func (e *RealSwapExecutor) execute(p *SingleSwapParams) (int64, int64) {
144 caller := e.rlm.Previous().Address()
145 recipient := access.MustGetAddress(prbac.ROLE_ROUTER.String())
146
147 return e.router.swapInner(
148 0,
149 e.rlm,
150 p.amountSpecified,
151 recipient, // if single swap => user will receive
152 p.SqrtPriceLimitX96(), // sqrtPriceLimitX96
153 newSwapCallbackData(p, caller),
154 )
155}
156
157// DrySwapExecutor implements SwapExecutor for dry swaps.
158type DrySwapExecutor struct {
159 router *routerV1
160 // payer is the user's address resolved at the entry point (DrySwapRoute),
161 // since dry-run paths do not have a realm value to read PreviousRealm from.
162 payer address
163}
164
165// execute performs the dry swap execution.
166func (e *DrySwapExecutor) execute(p *SingleSwapParams) (int64, int64) {
167 return e.router.swapDryInner(
168 p.amountSpecified,
169 u256.Zero(),
170 newSwapCallbackData(p, e.payer),
171 )
172}
173
174// calculateSqrtPriceLimitForSwap returns the price limit for a swap operation.
175// If a non-zero limit is provided by the caller, it is returned as-is.
176// Otherwise, returns the precomputed limit for the given fee tier and direction.
177// The returned pointer is shared (read-only — callers must not mutate it).
178func calculateSqrtPriceLimitForSwap(zeroForOne bool, fee uint32, sqrtPriceLimitX96 *u256.Uint) *u256.Uint {
179 if !sqrtPriceLimitX96.IsZero() {
180 return sqrtPriceLimitX96
181 }
182
183 if zeroForOne {
184 return mustGetSqrtPriceLimit(sqrtPriceLimitForward, fee)
185 }
186 return mustGetSqrtPriceLimit(sqrtPriceLimitBackward, fee)
187}
188
189func mustGetSqrtPriceLimit(limits map[uint32]*u256.Uint, fee uint32) *u256.Uint {
190 limit, ok := limits[fee]
191 if !ok {
192 panic(addDetailToError(
193 errInvalidPoolFeeTier,
194 ufmt.Sprintf("unknown fee(%d)", fee),
195 ))
196 }
197 return limit
198}
199
200// getMinTick returns the minimum tick value for a given fee tier.
201// The implementation follows Uniswap V3's tick spacing rules where
202// lower fee tiers allow for finer price granularity.
203func getMinTick(fee uint32) int32 {
204 switch fee {
205 case 100:
206 return -887272
207 case 500:
208 return -887270
209 case 3000:
210 return -887220
211 case 10000:
212 return -887200
213 default:
214 panic(addDetailToError(
215 errInvalidPoolFeeTier,
216 ufmt.Sprintf("unknown fee(%d)", fee),
217 ))
218 }
219}
220
221// getMaxTick returns the maximum tick value for a given fee tier.
222// The max tick values are the exact negatives of min tick values.
223func getMaxTick(fee uint32) int32 {
224 switch fee {
225 case 100:
226 return 887272
227 case 500:
228 return 887270
229 case 3000:
230 return 887220
231 case 10000:
232 return 887200
233 default:
234 panic(addDetailToError(
235 errInvalidPoolFeeTier,
236 ufmt.Sprintf("unknown fee(%d)", fee),
237 ))
238 }
239}