router.gno
6.05 Kb · 213 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 errExactOutAmountExceeded = "received more than requested in requested=%d, actual=%d"
14
15 // route validation
16 errInvalidRouteLength = "route length(%d) must be 1~7"
17
18 // quote validation
19 errRoutesQuotesMismatch = "mismatch between routes(%d) and quotes(%d) length"
20 errInvalidQuote = "invalid quote(%s) at index(%d)"
21 errInvalidQuoteValue = "quote(%s) at index(%d) must be positive value"
22 errQuoteExceedsMax = "quote(%s) at index(%d) must be less than or equal to %d"
23 errQuoteSumExceedsMax = "quote sum exceeds 100 at index(%d)"
24 errInvalidQuoteSum = "quote sum(%d) must be 100"
25
26 // swap type validation
27 errExactInTooFewReceived = "ExactIn: too few received (min:%d, got:%d)"
28 errExactOutTooMuchSpent = "ExactOut: too much spent (max:%d, used:%d)"
29
30 // route parsing validation
31 errEmptyRoutes = "routes cannot be empty"
32)
33
34// SwapValidator provides validation methods for swap operations
35type SwapValidator struct{}
36
37// exactOutAmount checks if output amount meets specified requirements.
38// For exact-out swaps, the output must be exactly the specified amount with tolerance up to swapCount units for rounding.
39func (v *SwapValidator) exactOutAmount(resultAmount, specifiedAmount int64, swapCount int64) error {
40 diff := int64(0)
41
42 if resultAmount >= specifiedAmount {
43 diff = resultAmount - specifiedAmount
44 } else {
45 diff = specifiedAmount - resultAmount
46 }
47
48 if diff > swapCount {
49 return ufmt.Errorf(errExactOutAmountExceeded, specifiedAmount, resultAmount)
50 }
51
52 return nil
53}
54
55// slippage ensures swap amounts meet slippage requirements.
56func (v *SwapValidator) slippage(swapType SwapType, amountIn, amountOut, limit int64) error {
57 switch swapType {
58 case ExactIn:
59 if amountOut < limit {
60 return ufmt.Errorf(errExactInTooFewReceived, limit, amountOut)
61 }
62 case ExactOut:
63 if amountIn > limit {
64 return ufmt.Errorf(errExactOutTooMuchSpent, limit, amountIn)
65 }
66 default:
67 return errors.New(errInvalidSwapType)
68 }
69 return nil
70}
71
72// swapType ensures the swap type string is valid.
73func (v *SwapValidator) swapType(swapTypeStr string) (SwapType, error) {
74 swapType, err := trySwapTypeFromStr(swapTypeStr)
75 if err != nil {
76 return Unknown, errors.New(errInvalidSwapType)
77 }
78 return swapType, nil
79}
80
81// amount ensures the amount is properly formatted and positive.
82func (v *SwapValidator) amount(amount int64) (int64, error) {
83 if amount <= 0 {
84 return 0, ufmt.Errorf(ErrInvalidPositiveAmount, amount)
85 }
86 return amount, nil
87}
88
89// amountLimit ensures the amount limit is properly formatted and non-zero.
90func (v *SwapValidator) amountLimit(amountLimit int64) (int64, error) {
91 if amountLimit <= 0 {
92 return 0, ufmt.Errorf(ErrInvalidZeroAmountLimit, amountLimit)
93 }
94 return amountLimit, nil
95}
96
97// RouteParser handles parsing and validation of routes and quotes
98type RouteParser struct{}
99
100// NewRouteParser creates a new route parser instance.
101func NewRouteParser() *RouteParser {
102 return &RouteParser{}
103}
104
105// ParseRoutes parses route and quote strings into slices and validates them.
106func (p *RouteParser) ParseRoutes(routes, quotes string) ([]string, []string, error) {
107 // Check for empty routes
108 if routes == "" || quotes == "" {
109 return nil, nil, errors.New(errEmptyRoutes)
110 }
111
112 routesArr := splitSingleChar(routes, ',')
113 quotesArr := splitSingleChar(quotes, ',')
114
115 if err := p.ValidateRoutesAndQuotes(routesArr, quotesArr); err != nil {
116 return nil, nil, err
117 }
118
119 return routesArr, quotesArr, nil
120}
121
122// ValidateRoutesAndQuotes ensures routes and quotes meet required criteria.
123func (p *RouteParser) ValidateRoutesAndQuotes(routes, quotes []string) error {
124 rr := len(routes)
125 qq := len(quotes)
126
127 if rr < 1 || rr > 7 {
128 return ufmt.Errorf(errInvalidRouteLength, rr)
129 }
130
131 if rr != qq {
132 return ufmt.Errorf(errRoutesQuotesMismatch, rr, qq)
133 }
134
135 return p.ValidateQuoteSum(quotes)
136}
137
138// ValidateQuoteSum ensures all quotes add up to 100%.
139func (p *RouteParser) ValidateQuoteSum(quotes []string) error {
140 const (
141 maxQuote int8 = 100
142 minQuote int8 = 0
143 )
144
145 var sum int8
146
147 for i, quote := range quotes {
148 qt, err := strconv.ParseInt(quote, 10, 8)
149 if err != nil {
150 return ufmt.Errorf(errInvalidQuote, quote, i)
151 }
152 intQuote := int8(qt)
153
154 // Quote must be positive (> 0) as each route needs a non-zero allocation.
155 // A quote of 0 would mean no swap through that route, which is invalid.
156 if intQuote <= minQuote { // minQuote = 0, so this rejects quote = 0
157 return ufmt.Errorf(errInvalidQuoteValue, quote, i)
158 }
159
160 if intQuote > maxQuote {
161 return ufmt.Errorf(errQuoteExceedsMax, quote, i, maxQuote)
162 }
163
164 if sum > maxQuote-intQuote {
165 return ufmt.Errorf(errQuoteSumExceedsMax, i)
166 }
167
168 sum += intQuote
169 }
170
171 if sum != maxQuote {
172 return ufmt.Errorf(errInvalidQuoteSum, sum)
173 }
174
175 return nil
176}
177
178// finalizeSwap handles post-swap operations and validations.
179func (r *routerV1) finalizeSwap(
180 _ int,
181 rlm realm,
182 inputToken, outputToken string,
183 resultAmountIn, resultAmountOut int64,
184 swapType SwapType,
185 tokenAmountLimit int64,
186 amountSpecified int64,
187 swapCount int64,
188 isSetSqrtPriceLimitX96 bool,
189) (int64, int64) {
190 validator := &SwapValidator{}
191
192 // Handle swap fee
193 resultAmountOutWithoutFee := r.handleSwapFee(0, rlm, outputToken, resultAmountOut)
194
195 // Validate exact out amount if applicable
196 // If sqrtPriceLimitX96 is set, slippage check is not needed
197 if swapType == ExactOut && !isSetSqrtPriceLimitX96 {
198 if err := validator.exactOutAmount(resultAmountOutWithoutFee, amountSpecified, swapCount); err != nil {
199 panic(addDetailToError(errSlippage, err.Error()))
200 }
201 }
202
203 if err := validator.slippage(swapType, resultAmountIn, resultAmountOutWithoutFee, tokenAmountLimit); err != nil {
204 panic(addDetailToError(errSlippage, err.Error()))
205 }
206
207 return resultAmountIn, resultAmountOutWithoutFee
208}
209
210// validateRoutesAndQuotes is a convenience function that parses and validates routes in one call.
211func validateRoutesAndQuotes(routes, quotes string) ([]string, []string, error) {
212 return NewRouteParser().ParseRoutes(routes, quotes)
213}