package v1 import ( "chain/runtime" "strconv" u256 "gno.land/p/gnoswap/uint256" "gno.land/p/nt/ufmt" "gno.land/r/gnoland/wugnot" "gno.land/r/gnoswap/common" ) var one = u256.One() // ErrorMessages define all error message templates used throughout the router const ( // slippage validation errExactOutAmountExceeded = "received more than requested in requested=%d, actual=%d" // route validation errInvalidRouteLength = "route length(%d) must be 1~7" // quote validation errRoutesQuotesMismatch = "mismatch between routes(%d) and quotes(%d) length" errInvalidQuote = "invalid quote(%s) at index(%d)" errInvalidQuoteValue = "quote(%s) at index(%d) must be positive value" errQuoteExceedsMax = "quote(%s) at index(%d) must be less than or equal to %d" errQuoteSumExceedsMax = "quote sum exceeds 100 at index(%d)" errInvalidQuoteSum = "quote sum(%d) must be 100" // balance and overflow validation errOverflowInBalance = "overflow in balance calculation: beforeBalance(%d) + wrappedAmount(%d)" errTooMuchWugnotSpent = "too much wugnot spent (wrapped: %d, spend: %d)" // swap type validation errExactInTooFewReceived = "ExactIn: too few received (min:%d, got:%d)" errExactOutTooMuchSpent = "ExactOut: too much spent (max:%d, used:%d)" // route parsing validation errEmptyRoutes = "routes cannot be empty" ) // GnotSwapHandler encapsulates methods for handling GNOT token swaps type GnotSwapHandler struct { BeforeBalance int64 WrappedAmount int64 NewBalance int64 } // newGnotSwapHandler creates a new handler for GNOT swaps. func newGnotSwapHandler(beforeBalance, wrappedAmount int64) *GnotSwapHandler { return &GnotSwapHandler{ BeforeBalance: beforeBalance, WrappedAmount: wrappedAmount, } } // UpdateNewBalance updates the current balance after swap operations. func (h *GnotSwapHandler) UpdateNewBalance() { h.NewBalance = wugnot.BalanceOf(runtime.PreviousRealm().Address()) } // HandleInputSwap manages unwrapping logic for GNOT input tokens. // // When user sends native GNOT for swap: // 1. GNOT was wrapped to WUGNOT at start // 2. Swap consumed some WUGNOT // 3. Remaining WUGNOT must be unwrapped back to GNOT // // Returns: // - error: overflow/insufficient balance errors, or unwrap errors func (h *GnotSwapHandler) HandleInputSwap() error { // Check for overflow when adding balances if h.BeforeBalance > 0 && h.WrappedAmount > 0 { if h.BeforeBalance > (1<<63-1)-h.WrappedAmount { return ufmt.Errorf(errOverflowInBalance, h.BeforeBalance, h.WrappedAmount) } } totalBefore := h.BeforeBalance + h.WrappedAmount spend := totalBefore - h.NewBalance if spend > h.WrappedAmount { return ufmt.Errorf(errTooMuchWugnotSpent, h.WrappedAmount, spend) } toUnwrap := h.WrappedAmount - spend caller := runtime.PreviousRealm().Address() return unwrapWithTransferFrom(caller, caller, toUnwrap) } // SwapValidator provides validation methods for swap operations type SwapValidator struct{} // exactOutAmount checks if output amount meets specified requirements. // For exact-out swaps, the output must be exactly the specified amount with tolerance up to swapCount units for rounding. func (v *SwapValidator) exactOutAmount(resultAmount, specifiedAmount int64, swapCount int64) error { diff := int64(0) if resultAmount >= specifiedAmount { diff = resultAmount - specifiedAmount } else { diff = specifiedAmount - resultAmount } if diff > swapCount { return ufmt.Errorf(errExactOutAmountExceeded, specifiedAmount, resultAmount) } return nil } // slippage ensures swap amounts meet slippage requirements. func (v *SwapValidator) slippage(swapType SwapType, amountIn, amountOut, limit int64) error { switch swapType { case ExactIn: if amountOut < limit { return ufmt.Errorf(errExactInTooFewReceived, limit, amountOut) } case ExactOut: if amountIn > limit { return ufmt.Errorf(errExactOutTooMuchSpent, limit, amountIn) } default: return errInvalidSwapType } return nil } // swapType ensures the swap type string is valid. func (v *SwapValidator) swapType(swapTypeStr string) (SwapType, error) { swapType, err := trySwapTypeFromStr(swapTypeStr) if err != nil { return Unknown, errInvalidSwapType } return swapType, nil } // amount ensures the amount is properly formatted and positive. func (v *SwapValidator) amount(amount int64) (int64, error) { if amount <= 0 { return 0, ufmt.Errorf(ErrInvalidPositiveAmount, amount) } return amount, nil } // amountLimit ensures the amount limit is properly formatted and non-zero. func (v *SwapValidator) amountLimit(amountLimit int64) (int64, error) { if amountLimit <= 0 { return 0, ufmt.Errorf(ErrInvalidZeroAmountLimit, amountLimit) } return amountLimit, nil } // RouteParser handles parsing and validation of routes and quotes type RouteParser struct{} // NewRouteParser creates a new route parser instance. func NewRouteParser() *RouteParser { return &RouteParser{} } // ParseRoutes parses route and quote strings into slices and validates them. func (p *RouteParser) ParseRoutes(routes, quotes string) ([]string, []string, error) { // Check for empty routes if routes == "" || quotes == "" { return nil, nil, ufmt.Errorf(errEmptyRoutes) } routesArr := splitSingleChar(routes, ',') quotesArr := splitSingleChar(quotes, ',') if err := p.ValidateRoutesAndQuotes(routesArr, quotesArr); err != nil { return nil, nil, err } return routesArr, quotesArr, nil } // ValidateRoutesAndQuotes ensures routes and quotes meet required criteria. func (p *RouteParser) ValidateRoutesAndQuotes(routes, quotes []string) error { rr := len(routes) qq := len(quotes) if rr < 1 || rr > 7 { return ufmt.Errorf(errInvalidRouteLength, rr) } if rr != qq { return ufmt.Errorf(errRoutesQuotesMismatch, rr, qq) } return p.ValidateQuoteSum(quotes) } // ValidateQuoteSum ensures all quotes add up to 100%. func (p *RouteParser) ValidateQuoteSum(quotes []string) error { const ( maxQuote int8 = 100 minQuote int8 = 0 ) var sum int8 for i, quote := range quotes { qt, err := strconv.ParseInt(quote, 10, 8) if err != nil { return ufmt.Errorf(errInvalidQuote, quote, i) } intQuote := int8(qt) // Quote must be positive (> 0) as each route needs a non-zero allocation. // A quote of 0 would mean no swap through that route, which is invalid. if intQuote <= minQuote { // minQuote = 0, so this rejects quote = 0 return ufmt.Errorf(errInvalidQuoteValue, quote, i) } if intQuote > maxQuote { return ufmt.Errorf(errQuoteExceedsMax, quote, i, maxQuote) } if sum > maxQuote-intQuote { return ufmt.Errorf(errQuoteSumExceedsMax, i) } sum += intQuote } if sum != maxQuote { return ufmt.Errorf(errInvalidQuoteSum, sum) } return nil } // finalizeSwap handles post-swap operations and validations. func (r *routerV1) finalizeSwap( inputToken, outputToken string, resultAmountIn, resultAmountOut int64, swapType SwapType, tokenAmountLimit int64, userBeforeWugnotBalance, userWrappedWugnot int64, amountSpecified int64, swapCount int64, isSetSqrtPriceLimitX96 bool, ) (int64, int64) { validator := &SwapValidator{} // Handle swap fee resultAmountOutWithoutFee := r.handleSwapFee(outputToken, resultAmountOut) // Validate exact out amount if applicable // If sqrtPriceLimitX96 is set, slippage check is not needed if swapType == ExactOut && !isSetSqrtPriceLimitX96 { if err := validator.exactOutAmount(resultAmountOutWithoutFee, amountSpecified, swapCount); err != nil { panic(addDetailToError(errSlippage, err.Error())) } } // Handle GNOT token swaps handler := newGnotSwapHandler(userBeforeWugnotBalance, userWrappedWugnot) handler.UpdateNewBalance() // Handle input GNOT token swap if common.IsGNOTNativePath(inputToken) { if err := handler.HandleInputSwap(); err != nil { panic(err) } } if err := validator.slippage(swapType, resultAmountIn, resultAmountOutWithoutFee, tokenAmountLimit); err != nil { panic(addDetailToError(errSlippage, err.Error())) } return resultAmountIn, resultAmountOutWithoutFee } // validateRoutesAndQuotes is a convenience function that parses and validates routes in one call. func validateRoutesAndQuotes(routes, quotes string) ([]string, []string, error) { return NewRouteParser().ParseRoutes(routes, quotes) }