router_dry.gno
10.45 Kb · 333 lines
1package router
2
3import (
4 "errors"
5 "strings"
6
7 "gno.land/p/gnoswap/gnsmath/v1"
8 "gno.land/p/gnoswap/utils/v1"
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 ErrInvalidQuoteRange = "quote(%d) must be %d~%d"
26)
27
28// SwapProcessor handles the execution of swap operations
29type SwapProcessor struct {
30 router *routerV1
31}
32
33// ProcessSingleSwap simulates one pool hop without executing transfers.
34//
35// Parameters:
36// - route: single pool path formatted as TOKEN0:TOKEN1:FEE
37// - amountSpecified: signed input or output amount for the hop
38//
39// Returns:
40// - amountIn: input amount the pool would receive
41// - amountOut: output amount the pool would send
42// - err: always nil when the dry pool call returns; malformed paths or pool failures panic in the underlying helpers
43func (p *SwapProcessor) ProcessSingleSwap(route string, amountSpecified int64) (amountIn, amountOut int64, err error) {
44 input, output, fee := getDataForSinglePath(route)
45 singleParams := SingleSwapParams{
46 tokenIn: input,
47 tokenOut: output,
48 fee: fee,
49 amountSpecified: amountSpecified,
50 }
51
52 amountIn, amountOut = p.router.singleDrySwap(&singleParams)
53 return amountIn, amountOut, nil
54}
55
56// ProcessMultiSwap simulates a multi-hop route in the requested direction.
57//
58// Parameters:
59// - swapType: ExactIn for forward execution or ExactOut for backward quoting
60// - route: multi-hop path with POOL_SEPARATOR between hops
61// - numHops: number of pools in route
62// - amountSpecified: signed input or output amount for the route
63//
64// Returns:
65// - amountIn: input amount the route would consume
66// - amountOut: output amount the route would produce
67// - err: nil on successful simulation; invalid swap-type error or downstream dry-swap validation error otherwise (pool query failures panic)
68func (p *SwapProcessor) ProcessMultiSwap(
69 swapType SwapType,
70 route string,
71 numHops int,
72 amountSpecified int64,
73) (int64, int64, error) {
74 pathIndex := getPathIndex(swapType, numHops)
75
76 input, output, fee := getDataForMultiPath(route, pathIndex)
77 singleParams := SingleSwapParams{
78 tokenIn: input,
79 tokenOut: output,
80 fee: fee,
81 amountSpecified: amountSpecified,
82 }
83
84 switch swapType {
85 case ExactIn:
86 return p.router.multiDrySwap(singleParams, numHops, route)
87 case ExactOut:
88 return p.router.multiDrySwapNegative(singleParams, numHops, route)
89 default:
90 return 0, 0, ufmt.Errorf(ErrUnknownSwapType, swapType)
91 }
92}
93
94// ValidateSwapResults checks simulated amounts against exactness and slippage constraints.
95//
96// Parameters:
97// - swapType: ExactIn or ExactOut validation direction
98// - resultAmountIn: simulated aggregate input amount
99// - resultAmountOut: simulated aggregate output amount after router fee
100// - amountSpecified: caller's exact input or output target
101// - amountLimit: minimum output or maximum input slippage bound
102// - swapCount: number of pool hops used for exact-output tolerance
103//
104// Returns:
105// - amountIn: resultAmountIn when validation reaches the return path
106// - amountOut: resultAmountOut when validation reaches the return path
107// - err: nil when liquidity, exactness, and slippage checks pass; relevant validation error otherwise
108func (p *SwapProcessor) ValidateSwapResults(
109 swapType SwapType,
110 resultAmountIn, resultAmountOut int64,
111 amountSpecified, amountLimit int64,
112 swapCount int64,
113) (amountIn, amountOut int64, err error) {
114 if resultAmountIn == 0 || resultAmountOut == 0 {
115 return 0, 0, errors.New(errInsufficientLiquidity)
116 }
117
118 validator := &SwapValidator{}
119
120 if swapType == ExactOut {
121 absSpecified := gnsmath.SafeAbsInt64(amountSpecified)
122
123 if err := validator.exactOutAmount(resultAmountOut, absSpecified, swapCount); err != nil {
124 return resultAmountIn, resultAmountOut, err
125 }
126 }
127
128 if swapType == ExactIn {
129 if err := validator.exactInAmount(resultAmountIn, amountSpecified); err != nil {
130 return resultAmountIn, resultAmountOut, err
131 }
132 }
133
134 if err := validator.slippage(swapType, resultAmountIn, resultAmountOut, amountLimit); err != nil {
135 return resultAmountIn, resultAmountOut, err
136 }
137
138 return resultAmountIn, resultAmountOut, nil
139}
140
141// AddSwapResults adds one route's amounts to accumulated totals using overflow-safe arithmetic.
142//
143// Parameters:
144// - resultAmountIn: accumulated input before this route
145// - resultAmountOut: accumulated output before this route
146// - amountIn: current route input amount
147// - amountOut: current route output amount
148//
149// Returns:
150// - newAmountIn: accumulated input after adding amountIn
151// - newAmountOut: accumulated output after adding amountOut
152// - err: always nil; overflow is raised by safe arithmetic rather than returned
153func (p *SwapProcessor) AddSwapResults(
154 resultAmountIn, resultAmountOut, amountIn, amountOut int64,
155) (int64, int64, error) {
156 newAmountIn := gnsmath.SafeAddInt64(resultAmountIn, amountIn)
157 newAmountOut := gnsmath.SafeAddInt64(resultAmountOut, amountOut)
158
159 return newAmountIn, newAmountOut, nil
160}
161
162// DrySwapRoute simulates a token swap route without executing transfers or changing state.
163//
164// Parameters:
165// - inputToken: input token contract path
166// - outputToken: output token contract path
167// - specifiedAmount: exact input amount for EXACT_IN or exact output amount for EXACT_OUT
168// - swapTypeStr: EXACT_IN or EXACT_OUT
169// - strRouteArr: comma-separated route paths, with at most seven route entries
170// - quoteArr: comma-separated route allocation percentages summing to 100
171// - tokenAmountLimit: minimum output for EXACT_IN or maximum input for EXACT_OUT
172//
173// Returns:
174// - amountIn: simulated input amount encoded as a decimal string
175// - amountOut: simulated net output amount encoded as a decimal string
176// - err: nil on success; validation, liquidity, parsing, or slippage error otherwise
177func (r *routerV1) DrySwapRoute(
178 inputToken, outputToken string,
179 specifiedAmount string,
180 swapTypeStr string,
181 strRouteArr, quoteArr string,
182 tokenAmountLimit string,
183) (amountIn, amountOut string, err error) {
184 defer func() {
185 if r := recover(); r != nil {
186 amountIn = "0"
187 amountOut = "0"
188 if recoveredErr, ok := r.(error); ok {
189 err = recoveredErr
190 } else {
191 err = ufmt.Errorf("%v", r)
192 }
193 }
194 }()
195
196 inputAmount, outputAmount, err := r.drySwapRoute(
197 inputToken,
198 outputToken,
199 utils.SafeParseInt64(specifiedAmount),
200 swapTypeStr,
201 strRouteArr,
202 quoteArr,
203 utils.SafeParseInt64(tokenAmountLimit),
204 )
205 return utils.FormatInt(inputAmount), utils.FormatInt(outputAmount), err
206}
207
208// drySwapRoute is the internal dry-run swap simulation that returns errors
209// instead of panicking on validation or liquidity failures.
210func (r *routerV1) drySwapRoute(
211 inputToken, outputToken string,
212 specifiedAmount int64,
213 swapTypeStr string,
214 strRouteArr, quoteArr string,
215 tokenAmountLimit int64,
216) (int64, int64, error) {
217 if err := common.ValidateRegistered(inputToken, outputToken); err != nil {
218 return 0, 0, err
219 }
220
221 assertIsValidRoutePaths(strRouteArr, inputToken, outputToken)
222 assertIsExistsPools(strRouteArr)
223
224 // initialize components
225 validator := &SwapValidator{}
226 processor := &SwapProcessor{router: r}
227
228 // validate and parse inputs
229 swapType, err := validator.swapType(swapTypeStr)
230 if err != nil {
231 return 0, 0, makeErrorWithDetails(errInvalidSwapType, err.Error())
232 }
233
234 amountSpecified, err := validator.amount(specifiedAmount)
235 if err != nil {
236 return 0, 0, makeErrorWithDetails(errInvalidInput, err.Error())
237 }
238
239 routes, quotes, err := NewRouteParser().ParseRoutes(strRouteArr, quoteArr)
240 if err != nil {
241 return 0, 0, makeErrorWithDetails(errInvalidRoutesAndQuotes, err.Error())
242 }
243
244 swapFee := r.store.GetSwapFee()
245
246 // Store original amount for validation (before router fee adjustment)
247 originalAmountSpecified := amountSpecified
248
249 // adjust amount sign for exact out swaps
250 if swapType == ExactOut {
251 absSpecified := gnsmath.SafeAbsInt64(amountSpecified)
252 amountSpecifiedWithRouterFee := calculateExactOutWithRouterFee(absSpecified, swapFee)
253 amountSpecified = -amountSpecifiedWithRouterFee
254 }
255
256 // initialize accumulators for swap results
257 resultAmountIn, resultAmountOut := int64(0), int64(0)
258 remainRequestAmount := amountSpecified
259 swapCount := int64(0)
260
261 // Process each route
262 for i, route := range routes {
263 toSwapAmount := int64(0)
264
265 // if it's the last route, use the remaining amount
266 isLastRoute := i == len(routes)-1
267 if !isLastRoute {
268 // calculate the amount to swap for this route
269 swapAmount, err := calculateSwapAmountByQuote(amountSpecified, quotes[i])
270 if err != nil {
271 return 0, 0, err
272 }
273
274 // update the remaining amount
275 resultRemainRequestAmount := gnsmath.SafeSubInt64(remainRequestAmount, swapAmount)
276
277 remainRequestAmount = resultRemainRequestAmount
278 toSwapAmount = swapAmount
279 } else {
280 toSwapAmount = remainRequestAmount
281 }
282
283 // determine the number of hops and validate
284 numHops := strings.Count(route, POOL_SEPARATOR) + 1
285 assertHopsInRange(numHops)
286
287 // accumulate total swap count for validation
288 swapCount += int64(numHops)
289
290 // execute the appropriate swap type
291 var amountIn, amountOut int64
292 if numHops == 1 {
293 amountIn, amountOut, err = processor.ProcessSingleSwap(route, toSwapAmount)
294 } else {
295 amountIn, amountOut, err = processor.ProcessMultiSwap(swapType, route, numHops, toSwapAmount)
296 }
297
298 if err != nil {
299 return 0, 0, err
300 }
301
302 if amountIn == 0 || amountOut == 0 {
303 return 0, 0, errors.New(errInsufficientLiquidity)
304 }
305
306 // update accumulated results
307 resultAmountIn, resultAmountOut, err = processor.AddSwapResults(resultAmountIn, resultAmountOut, amountIn, amountOut)
308 if err != nil {
309 return 0, 0, err
310 }
311 }
312
313 // simulate deduct router fee
314 feeAmountInt64 := calculateRouterFee(resultAmountOut, swapFee)
315
316 resultAmountOut = gnsmath.SafeSubInt64(resultAmountOut, feeAmountInt64)
317
318 return processor.ValidateSwapResults(swapType, resultAmountIn, resultAmountOut, originalAmountSpecified, tokenAmountLimit, swapCount)
319}
320
321// getPathIndex returns the path index based on swap type and number of hops.
322func getPathIndex(swapType SwapType, numHops int) int {
323 switch swapType {
324 case ExactIn:
325 // first data for exact input swaps
326 return 0
327 case ExactOut:
328 // last data for exact output swaps
329 return numHops - 1
330 default:
331 panic("should not happen")
332 }
333}