router_dry.gno
9.59 Kb · 313 lines
1package router
2
3import (
4 "chain/runtime/unsafe"
5 "strings"
6
7 gnsmath "gno.land/p/gnoswap/gnsmath"
8 "gno.land/p/gnoswap/utils"
9 ufmt "gno.land/p/nt/ufmt/v0"
10
11 "gno.land/r/gnoswap/common"
12)
13
14// QuoteConstraints defines the valid range for swap quote percentages
15const (
16 MaxQuotePercentage = 100
17 MinQuotePercentage = 0
18 PERCENTAGE_DENOMINATOR = int64(100)
19)
20
21// ErrorMessages for DrySwapRoute operations
22const (
23 ErrUnknownSwapType = "unknown swapType(%s)"
24 ErrInvalidPositiveAmount = "invalid amount(%s), must be positive"
25 ErrInvalidZeroAmountLimit = "invalid amountLimit(%s), should not be zero"
26 ErrInvalidQuoteRange = "quote(%d) must be %d~%d"
27)
28
29// SwapProcessor handles the execution of swap operations
30type SwapProcessor struct {
31 router *routerV1
32 // payer identifies the user for dry-run simulations. It is resolved once at
33 // the entry point via unsafe.PreviousRealm since dry-run paths are
34 // read-only and carry no realm value to thread.
35 payer address
36}
37
38// ProcessSingleSwap handles a single-hop swap simulation.
39//
40// Parameters:
41// - route: Single pool path "TOKEN0:TOKEN1:FEE"
42// - amountSpecified: Input/output amount depending on swap type
43//
44// Returns:
45// - amountIn: Expected input amount
46// - amountOut: Expected output amount
47// - err: Error if swap simulation fails
48func (p *SwapProcessor) ProcessSingleSwap(route string, amountSpecified int64) (amountIn, amountOut int64, err error) {
49 input, output, fee := getDataForSinglePath(route)
50 singleParams := SingleSwapParams{
51 tokenIn: input,
52 tokenOut: output,
53 fee: fee,
54 amountSpecified: amountSpecified,
55 }
56
57 amountIn, amountOut = p.router.singleDrySwap(p.payer, &singleParams)
58 return amountIn, amountOut, nil
59}
60
61// ProcessMultiSwap handles a multi-hop swap simulation.
62//
63// Parameters:
64// - swapType: ExactIn or ExactOut
65// - route: Multi-hop route with POOL_SEPARATOR
66// - numHops: Number of hops in the route
67// - amountSpecified: Input/output amount depending on swap type
68//
69// Returns:
70// - amountIn: Expected input amount
71// - amountOut: Expected output amount
72// - err: Error if swap simulation fails
73func (p *SwapProcessor) ProcessMultiSwap(
74 swapType SwapType,
75 route string,
76 numHops int,
77 amountSpecified int64,
78) (int64, int64, error) {
79 recipient := p.payer
80 pathIndex := getPathIndex(swapType, numHops)
81
82 input, output, fee := getDataForMultiPath(route, pathIndex)
83 swapParams := newSwapParams(input, output, fee, recipient, amountSpecified)
84
85 switch swapType {
86 case ExactIn:
87 return p.router.multiDrySwap(p.payer, *swapParams, numHops, route)
88 case ExactOut:
89 return p.router.multiDrySwapNegative(p.payer, *swapParams, numHops, route)
90 default:
91 return 0, 0, ufmt.Errorf(ErrUnknownSwapType, swapType)
92 }
93}
94
95// ValidateSwapResults checks if the swap results meet the required constraints.
96//
97// Parameters:
98// - swapType: ExactIn or ExactOut
99// - resultAmountIn, resultAmountOut: Swap simulation results
100// - amountSpecified: User's specified exact amount
101// - amountLimit: Slippage protection limit
102// - swapCount: Number of swap operations (for tolerance)
103//
104// Returns:
105// - amountIn: Input amount (same as resultAmountIn)
106// - amountOut: Output amount (same as resultAmountOut)
107// - success: true if slippage constraints are met, false otherwise
108func (p *SwapProcessor) ValidateSwapResults(
109 swapType SwapType,
110 resultAmountIn, resultAmountOut int64,
111 amountSpecified, amountLimit int64,
112 swapCount int64,
113) (amountIn, amountOut int64, success bool) {
114 if resultAmountIn == 0 || resultAmountOut == 0 {
115 return 0, 0, false
116 }
117
118 validator := &SwapValidator{}
119
120 if swapType == ExactOut {
121 if err := validator.exactOutAmount(resultAmountOut, gnsmath.SafeAbsInt64(amountSpecified), swapCount); err != nil {
122 return resultAmountIn, resultAmountOut, false
123 }
124 }
125
126 if err := validator.slippage(swapType, resultAmountIn, resultAmountOut, amountLimit); err != nil {
127 return resultAmountIn, resultAmountOut, false
128 }
129
130 return resultAmountIn, resultAmountOut, true
131}
132
133// AddSwapResults safely adds swap result amounts, checking for overflow.
134//
135// Parameters:
136// - resultAmountIn, resultAmountOut: Accumulated results from previous routes
137// - amountIn, amountOut: Results from current route
138//
139// Returns:
140// - newAmountIn: resultAmountIn + amountIn
141// - newAmountOut: resultAmountOut + amountOut
142// - err: Always nil (uses safe addition that panics on overflow)
143func (p *SwapProcessor) AddSwapResults(
144 resultAmountIn, resultAmountOut, amountIn, amountOut int64,
145) (int64, int64, error) {
146 newAmountIn := gnsmath.SafeAddInt64(resultAmountIn, amountIn)
147 newAmountOut := gnsmath.SafeAddInt64(resultAmountOut, amountOut)
148
149 return newAmountIn, newAmountOut, nil
150}
151
152// DrySwapRoute simulates a token swap route without executing the swap.
153//
154// Calculates expected amounts without modifying any state.
155// Useful for price quotes, UI previews, and slippage estimation.
156//
157// Parameters:
158// - inputToken, outputToken: Token contract paths
159// - specifiedAmount: Input amount (ExactIn) or output amount (ExactOut)
160// - swapTypeStr: "EXACT_IN" or "EXACT_OUT"
161// - strRouteArr: Swap routes (comma-separated, max 7)
162// - quoteArr: Route split percentages (must sum to 100)
163// - tokenAmountLimit: Min output (ExactIn) or max input (ExactOut)
164//
165// Returns:
166// - amountIn: Expected input amount as string
167// - amountOut: Expected output amount as string
168// - success: true if swap would succeed, false if slippage/validation fails
169//
170// Note: Does not validate deadline or execute actual transfers.
171func (r *routerV1) DrySwapRoute(
172 inputToken, outputToken string,
173 specifiedAmount string,
174 swapTypeStr string,
175 strRouteArr, quoteArr string,
176 tokenAmountLimit string,
177) (string, string, bool) {
178 inputAmount, outputAmount, success := r.drySwapRoute(
179 inputToken,
180 outputToken,
181 utils.SafeParseInt64(specifiedAmount),
182 swapTypeStr,
183 strRouteArr,
184 quoteArr,
185 utils.SafeParseInt64(tokenAmountLimit),
186 )
187
188 return utils.FormatInt(inputAmount), utils.FormatInt(outputAmount), success
189}
190
191// drySwapRoute is a function for applying cross realm.
192func (r *routerV1) drySwapRoute(
193 inputToken, outputToken string,
194 specifiedAmount int64,
195 swapTypeStr string,
196 strRouteArr, quoteArr string,
197 tokenAmountLimit int64,
198) (int64, int64, bool) {
199 common.MustRegistered(inputToken, outputToken)
200 // initialize components
201 validator := &SwapValidator{}
202 processor := &SwapProcessor{router: r, payer: unsafe.PreviousRealm().Address()}
203
204 // validate and parse inputs
205 swapType, err := validator.swapType(swapTypeStr)
206 if err != nil {
207 panic(addDetailToError(errInvalidSwapType, err.Error()))
208 }
209
210 amountSpecified, err := validator.amount(specifiedAmount)
211 if err != nil {
212 panic(addDetailToError(errInvalidInput, err.Error()))
213 }
214
215 amountLimit, err := validator.amountLimit(tokenAmountLimit)
216 if err != nil {
217 panic(addDetailToError(errInvalidInput, err.Error()))
218 }
219
220 routes, quotes, err := NewRouteParser().ParseRoutes(strRouteArr, quoteArr)
221 if err != nil {
222 panic(addDetailToError(errInvalidRoutesAndQuotes, err.Error()))
223 }
224
225 swapFee := r.store.GetSwapFee()
226
227 // Store original amount for validation (before router fee adjustment)
228 originalAmountSpecified := amountSpecified
229
230 // adjust amount sign for exact out swaps
231 if swapType == ExactOut {
232 amountSpecifiedWithRouterFee := calculateExactOutWithRouterFee(gnsmath.SafeAbsInt64(amountSpecified), swapFee)
233 amountSpecified = -amountSpecifiedWithRouterFee
234 }
235
236 // initialize accumulators for swap results
237 resultAmountIn, resultAmountOut := int64(0), int64(0)
238 remainRequestAmount := amountSpecified
239 swapCount := int64(0)
240
241 // Process each route
242 for i, route := range routes {
243 toSwapAmount := int64(0)
244
245 // if it's the last route, use the remaining amount
246 isLastRoute := i == len(routes)-1
247 if !isLastRoute {
248 // calculate the amount to swap for this route
249 swapAmount, err := calculateSwapAmountByQuote(amountSpecified, quotes[i])
250 if err != nil {
251 return 0, 0, false
252 }
253
254 // update the remaining amount
255 resultRemainRequestAmount := gnsmath.SafeSubInt64(remainRequestAmount, swapAmount)
256
257 remainRequestAmount = resultRemainRequestAmount
258 toSwapAmount = swapAmount
259 } else {
260 toSwapAmount = remainRequestAmount
261 }
262
263 // determine the number of hops and validate
264 numHops := strings.Count(route, POOL_SEPARATOR) + 1
265 assertHopsInRange(numHops)
266
267 // accumulate total swap count for validation
268 swapCount += int64(numHops)
269
270 // execute the appropriate swap type
271 var amountIn, amountOut int64
272 if numHops == 1 {
273 amountIn, amountOut, err = processor.ProcessSingleSwap(route, toSwapAmount)
274 } else {
275 amountIn, amountOut, err = processor.ProcessMultiSwap(swapType, route, numHops, toSwapAmount)
276 }
277
278 if err != nil {
279 panic(addDetailToError(errInvalidSwapType, err.Error()))
280 }
281
282 if amountIn == 0 || amountOut == 0 {
283 return 0, 0, false
284 }
285
286 // update accumulated results
287 resultAmountIn, resultAmountOut, err = processor.AddSwapResults(resultAmountIn, resultAmountOut, amountIn, amountOut)
288 if err != nil {
289 panic(addDetailToError(errInvalidInput, err.Error()))
290 }
291 }
292
293 // simulate deduct router fee
294 feeAmountInt64 := calculateRouterFee(resultAmountOut, swapFee)
295
296 resultAmountOut = gnsmath.SafeSubInt64(resultAmountOut, feeAmountInt64)
297
298 return processor.ValidateSwapResults(swapType, resultAmountIn, resultAmountOut, originalAmountSpecified, amountLimit, swapCount)
299}
300
301// getPathIndex returns the path index based on swap type and number of hops.
302func getPathIndex(swapType SwapType, numHops int) int {
303 switch swapType {
304 case ExactIn:
305 // first data for exact input swaps
306 return 0
307 case ExactOut:
308 // last data for exact output swaps
309 return numHops - 1
310 default:
311 panic("should not happen")
312 }
313}