router.gno
7.46 Kb · 249 lines
1package router
2
3import (
4 "errors"
5 "strconv"
6
7 ufmt "gno.land/p/nt/ufmt/v0"
8)
9
10// ErrorMessages define all error message templates used throughout the router
11const (
12 // slippage validation
13 errExactInAmountMismatch = "consumed input does not match requested (requested=%d, consumed=%d)"
14 errExactOutAmountExceeded = "received more than requested in requested=%d, actual=%d"
15
16 // route validation
17 errInvalidRouteLength = "route length(%d) must be 1~7"
18
19 // quote validation
20 errRoutesQuotesMismatch = "mismatch between routes(%d) and quotes(%d) length"
21 errInvalidQuote = "invalid quote(%s) at index(%d)"
22 errInvalidQuoteValue = "quote(%s) at index(%d) must be positive value"
23 errQuoteExceedsMax = "quote(%s) at index(%d) must be less than or equal to %d"
24 errQuoteSumExceedsMax = "quote sum exceeds 100 at index(%d)"
25 errInvalidQuoteSum = "quote sum(%d) must be 100"
26
27 // swap type validation
28 errExactInTooFewReceived = "ExactIn: too few received (min:%d, got:%d)"
29 errExactOutTooMuchSpent = "ExactOut: too much spent (max:%d, used:%d)"
30
31 // route parsing validation
32 errEmptyRoutes = "routes cannot be empty"
33)
34
35// SwapValidator provides validation methods for swap operations
36type SwapValidator struct{}
37
38// exactOutAmount checks if output amount meets specified requirements.
39// For exact-out swaps, the output must be exactly the specified amount with tolerance up to swapCount units for rounding.
40func (v *SwapValidator) exactOutAmount(resultAmount, specifiedAmount int64, swapCount int64) error {
41 diff := int64(0)
42
43 if resultAmount >= specifiedAmount {
44 diff = resultAmount - specifiedAmount
45 } else {
46 diff = specifiedAmount - resultAmount
47 }
48
49 if diff > swapCount {
50 return ufmt.Errorf(errExactOutAmountExceeded, specifiedAmount, resultAmount)
51 }
52
53 return nil
54}
55
56// exactInAmount checks that the swap consumed exactly the requested input amount.
57func (v *SwapValidator) exactInAmount(resultAmount, specifiedAmount int64) error {
58 if resultAmount != specifiedAmount {
59 return ufmt.Errorf(errExactInAmountMismatch, specifiedAmount, resultAmount)
60 }
61
62 return nil
63}
64
65// slippage ensures swap amounts meet slippage requirements.
66func (v *SwapValidator) slippage(swapType SwapType, amountIn, amountOut, limit int64) error {
67 switch swapType {
68 case ExactIn:
69 if amountOut < limit {
70 return ufmt.Errorf(errExactInTooFewReceived, limit, amountOut)
71 }
72 case ExactOut:
73 if amountIn > limit {
74 return ufmt.Errorf(errExactOutTooMuchSpent, limit, amountIn)
75 }
76 default:
77 return errors.New(errInvalidSwapType)
78 }
79 return nil
80}
81
82// swapType ensures the swap type string is valid.
83func (v *SwapValidator) swapType(swapTypeStr string) (SwapType, error) {
84 swapType, err := trySwapTypeFromStr(swapTypeStr)
85 if err != nil {
86 return Unknown, errors.New(errInvalidSwapType)
87 }
88 return swapType, nil
89}
90
91// amount ensures the amount is properly formatted and positive.
92func (v *SwapValidator) amount(amount int64) (int64, error) {
93 if amount <= 0 {
94 return 0, ufmt.Errorf(ErrInvalidPositiveAmount, amount)
95 }
96 return amount, nil
97}
98
99// RouteParser handles parsing and validation of routes and quotes
100type RouteParser struct{}
101
102// NewRouteParser creates a parser for comma-separated route and quote strings.
103//
104// Returns:
105// - parser: new RouteParser instance
106func NewRouteParser() *RouteParser {
107 return &RouteParser{}
108}
109
110// ParseRoutes splits route and quote strings and validates their pairing.
111//
112// Parameters:
113// - routes: comma-separated route paths
114// - quotes: comma-separated route allocation percentages
115//
116// Returns:
117// - routePaths: parsed route path strings
118// - quoteValues: parsed quote strings corresponding one-to-one with routePaths
119// - err: nil when routes, counts, and quote sum are valid; validation error otherwise
120func (p *RouteParser) ParseRoutes(routes, quotes string) ([]string, []string, error) {
121 // Check for empty routes
122 if routes == "" || quotes == "" {
123 return nil, nil, errors.New(errEmptyRoutes)
124 }
125
126 routesArr := splitSingleChar(routes, ',')
127 quotesArr := splitSingleChar(quotes, ',')
128
129 if err := p.ValidateRoutesAndQuotes(routesArr, quotesArr); err != nil {
130 return nil, nil, err
131 }
132
133 return routesArr, quotesArr, nil
134}
135
136// ValidateRoutesAndQuotes checks route count, route/quote cardinality, and quote sum.
137//
138// Parameters:
139// - routes: parsed route path strings
140// - quotes: parsed quote percentage strings
141//
142// Returns:
143// - err: nil when one to seven routes match quote count and quotes sum to 100; validation error otherwise
144func (p *RouteParser) ValidateRoutesAndQuotes(routes, quotes []string) error {
145 rr := len(routes)
146 qq := len(quotes)
147
148 if rr < 1 || rr > 7 {
149 return ufmt.Errorf(errInvalidRouteLength, rr)
150 }
151
152 if rr != qq {
153 return ufmt.Errorf(errRoutesQuotesMismatch, rr, qq)
154 }
155
156 return p.ValidateQuoteSum(quotes)
157}
158
159// ValidateQuoteSum verifies that each quote is positive, at most 100, and totals 100.
160//
161// Parameters:
162// - quotes: route allocation percentage strings
163//
164// Returns:
165// - err: nil when all quotes parse and total 100; invalid value or sum error otherwise
166func (p *RouteParser) ValidateQuoteSum(quotes []string) error {
167 const (
168 maxQuote int8 = 100
169 minQuote int8 = 0
170 )
171
172 var sum int8
173
174 for i, quote := range quotes {
175 qt, err := strconv.ParseInt(quote, 10, 8)
176 if err != nil {
177 return ufmt.Errorf(errInvalidQuote, quote, i)
178 }
179 intQuote := int8(qt)
180
181 // Quote must be positive (> 0) as each route needs a non-zero allocation.
182 // A quote of 0 would mean no swap through that route, which is invalid.
183 if intQuote <= minQuote { // minQuote = 0, so this rejects quote = 0
184 return ufmt.Errorf(errInvalidQuoteValue, quote, i)
185 }
186
187 if intQuote > maxQuote {
188 return ufmt.Errorf(errQuoteExceedsMax, quote, i, maxQuote)
189 }
190
191 if sum > maxQuote-intQuote {
192 return ufmt.Errorf(errQuoteSumExceedsMax, i)
193 }
194
195 sum += intQuote
196 }
197
198 if sum != maxQuote {
199 return ufmt.Errorf(errInvalidQuoteSum, sum)
200 }
201
202 return nil
203}
204
205// finalizeSwap handles post-swap operations and validations.
206func (r *routerV1) finalizeSwap(
207 _ int,
208 rlm realm,
209 inputToken, outputToken string,
210 resultAmountIn, resultAmountOut int64,
211 swapType SwapType,
212 tokenAmountLimit int64,
213 amountSpecified int64,
214 swapCount int64,
215 isSetSqrtPriceLimitX96 bool,
216) (int64, int64) {
217 validator := &SwapValidator{}
218
219 // Handle swap fee
220 resultAmountOutWithoutFee := r.handleSwapFee(0, rlm, outputToken, resultAmountOut)
221
222 // Validate the exact-out target when no caller price limit is set. A
223 // nonzero single-hop limit permits partial output, so only the max-input
224 // slippage check below remains applicable.
225 if swapType == ExactOut && !isSetSqrtPriceLimitX96 {
226 if err := validator.exactOutAmount(resultAmountOutWithoutFee, amountSpecified, swapCount); err != nil {
227 panic(addDetailToError(errSlippage, err.Error()))
228 }
229 }
230
231 // Validate exact in amount if applicable
232 // If sqrtPriceLimitX96 is set, partial consumption up to the price limit is intended
233 if swapType == ExactIn && !isSetSqrtPriceLimitX96 {
234 if err := validator.exactInAmount(resultAmountIn, amountSpecified); err != nil {
235 panic(addDetailToError(errInsufficientLiquidity, err.Error()))
236 }
237 }
238
239 if err := validator.slippage(swapType, resultAmountIn, resultAmountOutWithoutFee, tokenAmountLimit); err != nil {
240 panic(addDetailToError(errSlippage, err.Error()))
241 }
242
243 return resultAmountIn, resultAmountOutWithoutFee
244}
245
246// validateRoutesAndQuotes is a convenience function that parses and validates routes in one call.
247func validateRoutesAndQuotes(routes, quotes string) ([]string, []string, error) {
248 return NewRouteParser().ParseRoutes(routes, quotes)
249}