router.gno
8.14 Kb ยท 285 lines
1package v1
2
3import (
4 "chain/runtime"
5 "strconv"
6
7 u256 "gno.land/p/gnoswap/uint256"
8 "gno.land/p/nt/ufmt"
9
10 "gno.land/r/gnoland/wugnot"
11 "gno.land/r/gnoswap/common"
12)
13
14var one = u256.One()
15
16// ErrorMessages define all error message templates used throughout the router
17const (
18 // slippage validation
19 errExactOutAmountExceeded = "received more than requested in requested=%d, actual=%d"
20
21 // route validation
22 errInvalidRouteLength = "route length(%d) must be 1~7"
23
24 // quote validation
25 errRoutesQuotesMismatch = "mismatch between routes(%d) and quotes(%d) length"
26 errInvalidQuote = "invalid quote(%s) at index(%d)"
27 errInvalidQuoteValue = "quote(%s) at index(%d) must be positive value"
28 errQuoteExceedsMax = "quote(%s) at index(%d) must be less than or equal to %d"
29 errQuoteSumExceedsMax = "quote sum exceeds 100 at index(%d)"
30 errInvalidQuoteSum = "quote sum(%d) must be 100"
31
32 // balance and overflow validation
33 errOverflowInBalance = "overflow in balance calculation: beforeBalance(%d) + wrappedAmount(%d)"
34 errTooMuchWugnotSpent = "too much wugnot spent (wrapped: %d, spend: %d)"
35
36 // swap type validation
37 errExactInTooFewReceived = "ExactIn: too few received (min:%d, got:%d)"
38 errExactOutTooMuchSpent = "ExactOut: too much spent (max:%d, used:%d)"
39
40 // route parsing validation
41 errEmptyRoutes = "routes cannot be empty"
42)
43
44// GnotSwapHandler encapsulates methods for handling GNOT token swaps
45type GnotSwapHandler struct {
46 BeforeBalance int64
47 WrappedAmount int64
48 NewBalance int64
49}
50
51// newGnotSwapHandler creates a new handler for GNOT swaps.
52func newGnotSwapHandler(beforeBalance, wrappedAmount int64) *GnotSwapHandler {
53 return &GnotSwapHandler{
54 BeforeBalance: beforeBalance,
55 WrappedAmount: wrappedAmount,
56 }
57}
58
59// UpdateNewBalance updates the current balance after swap operations.
60func (h *GnotSwapHandler) UpdateNewBalance() {
61 h.NewBalance = wugnot.BalanceOf(runtime.PreviousRealm().Address())
62}
63
64// HandleInputSwap manages unwrapping logic for GNOT input tokens.
65//
66// When user sends native GNOT for swap:
67// 1. GNOT was wrapped to WUGNOT at start
68// 2. Swap consumed some WUGNOT
69// 3. Remaining WUGNOT must be unwrapped back to GNOT
70//
71// Returns:
72// - error: overflow/insufficient balance errors, or unwrap errors
73func (h *GnotSwapHandler) HandleInputSwap() error {
74 // Check for overflow when adding balances
75 if h.BeforeBalance > 0 && h.WrappedAmount > 0 {
76 if h.BeforeBalance > (1<<63-1)-h.WrappedAmount {
77 return ufmt.Errorf(errOverflowInBalance,
78 h.BeforeBalance, h.WrappedAmount)
79 }
80 }
81
82 totalBefore := h.BeforeBalance + h.WrappedAmount
83 spend := totalBefore - h.NewBalance
84
85 if spend > h.WrappedAmount {
86 return ufmt.Errorf(errTooMuchWugnotSpent,
87 h.WrappedAmount, spend)
88 }
89
90 toUnwrap := h.WrappedAmount - spend
91
92 caller := runtime.PreviousRealm().Address()
93 return unwrapWithTransferFrom(caller, caller, toUnwrap)
94}
95
96// SwapValidator provides validation methods for swap operations
97type SwapValidator struct{}
98
99// exactOutAmount checks if output amount meets specified requirements.
100// For exact-out swaps, the output must be exactly the specified amount with tolerance up to swapCount units for rounding.
101func (v *SwapValidator) exactOutAmount(resultAmount, specifiedAmount int64, swapCount int64) error {
102 diff := int64(0)
103
104 if resultAmount >= specifiedAmount {
105 diff = resultAmount - specifiedAmount
106 } else {
107 diff = specifiedAmount - resultAmount
108 }
109
110 if diff > swapCount {
111 return ufmt.Errorf(errExactOutAmountExceeded, specifiedAmount, resultAmount)
112 }
113
114 return nil
115}
116
117// slippage ensures swap amounts meet slippage requirements.
118func (v *SwapValidator) slippage(swapType SwapType, amountIn, amountOut, limit int64) error {
119 switch swapType {
120 case ExactIn:
121 if amountOut < limit {
122 return ufmt.Errorf(errExactInTooFewReceived, limit, amountOut)
123 }
124 case ExactOut:
125 if amountIn > limit {
126 return ufmt.Errorf(errExactOutTooMuchSpent, limit, amountIn)
127 }
128 default:
129 return errInvalidSwapType
130 }
131 return nil
132}
133
134// swapType ensures the swap type string is valid.
135func (v *SwapValidator) swapType(swapTypeStr string) (SwapType, error) {
136 swapType, err := trySwapTypeFromStr(swapTypeStr)
137 if err != nil {
138 return Unknown, errInvalidSwapType
139 }
140 return swapType, nil
141}
142
143// amount ensures the amount is properly formatted and positive.
144func (v *SwapValidator) amount(amount int64) (int64, error) {
145 if amount <= 0 {
146 return 0, ufmt.Errorf(ErrInvalidPositiveAmount, amount)
147 }
148 return amount, nil
149}
150
151// amountLimit ensures the amount limit is properly formatted and non-zero.
152func (v *SwapValidator) amountLimit(amountLimit int64) (int64, error) {
153 if amountLimit <= 0 {
154 return 0, ufmt.Errorf(ErrInvalidZeroAmountLimit, amountLimit)
155 }
156 return amountLimit, nil
157}
158
159// RouteParser handles parsing and validation of routes and quotes
160type RouteParser struct{}
161
162// NewRouteParser creates a new route parser instance.
163func NewRouteParser() *RouteParser {
164 return &RouteParser{}
165}
166
167// ParseRoutes parses route and quote strings into slices and validates them.
168func (p *RouteParser) ParseRoutes(routes, quotes string) ([]string, []string, error) {
169 // Check for empty routes
170 if routes == "" || quotes == "" {
171 return nil, nil, ufmt.Errorf(errEmptyRoutes)
172 }
173
174 routesArr := splitSingleChar(routes, ',')
175 quotesArr := splitSingleChar(quotes, ',')
176
177 if err := p.ValidateRoutesAndQuotes(routesArr, quotesArr); err != nil {
178 return nil, nil, err
179 }
180
181 return routesArr, quotesArr, nil
182}
183
184// ValidateRoutesAndQuotes ensures routes and quotes meet required criteria.
185func (p *RouteParser) ValidateRoutesAndQuotes(routes, quotes []string) error {
186 rr := len(routes)
187 qq := len(quotes)
188
189 if rr < 1 || rr > 7 {
190 return ufmt.Errorf(errInvalidRouteLength, rr)
191 }
192
193 if rr != qq {
194 return ufmt.Errorf(errRoutesQuotesMismatch, rr, qq)
195 }
196
197 return p.ValidateQuoteSum(quotes)
198}
199
200// ValidateQuoteSum ensures all quotes add up to 100%.
201func (p *RouteParser) ValidateQuoteSum(quotes []string) error {
202 const (
203 maxQuote int8 = 100
204 minQuote int8 = 0
205 )
206
207 var sum int8
208
209 for i, quote := range quotes {
210 qt, err := strconv.ParseInt(quote, 10, 8)
211 if err != nil {
212 return ufmt.Errorf(errInvalidQuote, quote, i)
213 }
214 intQuote := int8(qt)
215
216 // Quote must be positive (> 0) as each route needs a non-zero allocation.
217 // A quote of 0 would mean no swap through that route, which is invalid.
218 if intQuote <= minQuote { // minQuote = 0, so this rejects quote = 0
219 return ufmt.Errorf(errInvalidQuoteValue, quote, i)
220 }
221
222 if intQuote > maxQuote {
223 return ufmt.Errorf(errQuoteExceedsMax, quote, i, maxQuote)
224 }
225
226 if sum > maxQuote-intQuote {
227 return ufmt.Errorf(errQuoteSumExceedsMax, i)
228 }
229
230 sum += intQuote
231 }
232
233 if sum != maxQuote {
234 return ufmt.Errorf(errInvalidQuoteSum, sum)
235 }
236
237 return nil
238}
239
240// finalizeSwap handles post-swap operations and validations.
241func (r *routerV1) finalizeSwap(
242 inputToken, outputToken string,
243 resultAmountIn, resultAmountOut int64,
244 swapType SwapType,
245 tokenAmountLimit int64,
246 userBeforeWugnotBalance, userWrappedWugnot int64,
247 amountSpecified int64,
248 swapCount int64,
249 isSetSqrtPriceLimitX96 bool,
250) (int64, int64) {
251 validator := &SwapValidator{}
252
253 // Handle swap fee
254 resultAmountOutWithoutFee := r.handleSwapFee(outputToken, resultAmountOut)
255
256 // Validate exact out amount if applicable
257 // If sqrtPriceLimitX96 is set, slippage check is not needed
258 if swapType == ExactOut && !isSetSqrtPriceLimitX96 {
259 if err := validator.exactOutAmount(resultAmountOutWithoutFee, amountSpecified, swapCount); err != nil {
260 panic(addDetailToError(errSlippage, err.Error()))
261 }
262 }
263
264 // Handle GNOT token swaps
265 handler := newGnotSwapHandler(userBeforeWugnotBalance, userWrappedWugnot)
266 handler.UpdateNewBalance()
267
268 // Handle input GNOT token swap
269 if common.IsGNOTNativePath(inputToken) {
270 if err := handler.HandleInputSwap(); err != nil {
271 panic(err)
272 }
273 }
274
275 if err := validator.slippage(swapType, resultAmountIn, resultAmountOutWithoutFee, tokenAmountLimit); err != nil {
276 panic(addDetailToError(errSlippage, err.Error()))
277 }
278
279 return resultAmountIn, resultAmountOutWithoutFee
280}
281
282// validateRoutesAndQuotes is a convenience function that parses and validates routes in one call.
283func validateRoutesAndQuotes(routes, quotes string) ([]string, []string, error) {
284 return NewRouteParser().ParseRoutes(routes, quotes)
285}