Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}