Transaction

34B3EBB4D638E7…987362458121

Block 77,268 · index 0 · indexed

Summary

Hash
34B3EBB4D638E71488B4E4271CA2C5FDFAFAAE0FE8BE78554A5F987362458121
Block
77,268
Size
104903 bytes
Gas used
127,372,546 / 152,846,996
Fee
152847 ugnot
Status
success

Messages

#1AddPackagegno.land/r/gnoswap/router/v124 arguments

Arguments · 24

  1. #1router
  2. #2README.md
  3. #3# Router Swap routing engine for optimal trade execution across pools. ## Overview Router handles swap execution across multiple pools, finding optimal paths and managing slippage protection for traders. ## Configuration - **Router Fee**: 15 bps (0.15%) by default on output tokens; configurable by admin/governance from 0 through 1000 bps (0–10%) - **Max Hops**: 3 pools per route - **Deadline Buffer**: 5-30 minutes recommended for live swaps ## Core Functions ### `ExactInSwapRoute` Swaps an exact input amount for output, subject to a minimum net output. - Fixed input, variable output - The returned output is after the router fee - Reverts if output < amountOutMin - Supports multi-hop routing ### `ExactOutSwapRoute` Swaps for a requested final user output amount with maximum input. The `amountOut` target is post-router-fee: the router requests the corresponding gross pool output, deducts the fee, then validates and transfers the net output. - With no single-hop price limit, targets the requested post-fee output within the implementation's small per-hop rounding tolerance - A nonzero single-hop price limit may stop early and return a partial output - Reverts if input > amountInMax - Calculates path backwards ### `DrySwapRoute` Simulates a swap without execution. - Frontend price quotes - Slippage calculation - Path validation - No deadline check or token transfer ## Technical Details ### Route Format vs Pool Format - IMPORTANT DISTINCTION #### Route Format (Swap Direction) Routes in the router follow **swap direction ordering**: `tokenIn:tokenOut:fee` - First token = Input token (what you're swapping FROM) - Second token = Output token (what you're swapping TO) - This represents the actual flow of the swap Example for swapping GNS to WUGNOT: ``` gno.land/r/gnoswap/gns.GNS:gno.land/r/gnoland/wugnot.wugnot:3000 ``` #### Pool Format (Alphabetical) Pools are identified using **alphabetical ordering**: `token0:token1:fee` - token0 < token1 (lexicographically sorted) - This is the canonical pool identifier Example pool identifier (same pool as above): ``` gno.land/r/gnoland/wugnot.wugnot:gno.land/r/gnoswap/gns.GNS:3000 # gnoland/wugnot < gnoswap/gns alphabetically ``` #### Key Difference - **Router routes**: Follow your swap direction (BAR→BAZ means bar:baz in route) - **Pool identifiers**: Always alphabetically sorted (might be bar:baz or baz:bar) - The router automatically handles the conversion between these formats #### Native Token Route Specification **IMPORTANT**: Router swap functions do **not** accept native `ugnot` directly. - **Token Parameters**: Use token keys (`pkgPath.SYMBOL`) such as `"gno.land/r/gnoland/wugnot.wugnot"` - **Route Paths**: Also use token keys (`pkgPath.SYMBOL`) such as `"gno.land/r/gnoland/wugnot.wugnot"` This matches the current implementation: - Pools operate on token contract paths, including wrapped GNOT (`wugnot`) - Router swap entrypoints reject native-coin handling - Native-token refund and unwrap flows are not part of the current router implementation #### Route String Format Single-hop format: ``` tokenIn:tokenOut:fee ``` Multi-hop format (using _POOL_ separator): ``` tokenIn:tokenB:fee1*POOL*tokenB:tokenC:fee2*POOL*tokenC:tokenOut:fee3 ``` Single-hop example: ``` # Swapping GNS to WUGNOT Route: gno.land/r/gnoswap/gns.GNS:gno.land/r/gnoland/wugnot.wugnot:3000 # Router interprets: tokenIn=gns, tokenOut=wugnot, fee=3000 ``` Multi-hop example (GNS → WUGNOT → TOKEN_C): ``` # Each segment follows swap direction, connected by *POOL* gno.land/r/gnoswap/gns.GNS:gno.land/r/gnoland/wugnot.wugnot:3000*POOL*gno.land/r/gnoland/wugnot.wugnot:gno.land/r/<namespace>/token_c.TOKEN_C:500 ``` ### Quote Distribution Split large trades across routes to minimize impact: - `quoteArr`: positive percentage per route, with one quote for each route - Quotes must sum to 100; at most 7 routes are accepted - Example: "30,70" = 30% route1, 70% route2 ### Native Token Handling The current router implementation does **not** handle native `ugnot` directly. It rejects native-coin handling and routes swaps only through token keys (`pkgPath.SYMBOL`) such as wrapped GNOT (`wugnot`). #### Token Identifier Requirements - Use token keys (`pkgPath.SYMBOL`) such as `gno.land/r/gnoland/wugnot.wugnot` for both inputs/outputs and route specifications. - Do not pass `"ugnot"` as `inputToken` or `outputToken` to router swap functions. #### Approval and Transfer Requirements - Approve the router to spend the token contract you are swapping from. - If you want wrapped GNOT exposure, use the `wugnot` token contract path directly. - Native-token refund and unwrap flows are not part of the current router implementation. For live liquidity-changing swaps: - Set `amountOutMin = expected * (1 - slippage%)` - 0.5-1% for stable pairs - 1-3% for volatile pairs - Reverts if the net output is below the minimum ## Usage These snippets call the public domain proxy from a realm function with a current `cur` token. Import the proxy package and qualify its function names in integrating code. ### Basic Token Swaps ```go // Simple exact input swap amountIn, amountOut := ExactInSwapRoute( cross(cur), "gno.land/r/gnoswap/gns.GNS", // input token "gno.land/r/gnoland/wugnot.wugnot", // output token "1000000", // amount (6 decimals) "gno.land/r/gnoswap/gns.GNS:gno.land/r/gnoland/wugnot.wugnot:3000", // route "100", // 100% through route "950000", // min output time.Now().Unix() + 300, // deadline "g1referrer...", // referral ) // Multi-hop swap ExactInSwapRoute( cross(cur), "gno.land/r/gnoswap/gns.GNS", "gno.land/r/<namespace>/token_c.TOKEN_C", "1000000", "gno.land/r/gnoswap/gns.GNS:gno.land/r/gnoland/wugnot.wugnot:3000*POOL*gno.land/r/gnoland/wugnot.wugnot:gno.land/r/<namespace>/token_c.TOKEN_C:3000", "100", "900000", deadline, "", ) // Split route for large trades ExactInSwapRoute( cross(cur), "gno.land/r/gnoswap/gns.GNS", "gno.land/r/gnoland/wugnot.wugnot", "10000000000", "gno.land/r/gnoswap/gns.GNS:gno.land/r/gnoland/wugnot.wugnot:500,gno.land/r/gnoswap/gns.GNS:gno.land/r/gnoland/wugnot.wugnot:3000", "60,40", // 60% through 0.05%, 40% through 0.3% "9500000000", deadline, "", ) ``` Single-hop functions support partial execution through a nonzero `sqrtPriceLimitX96`: ```go // Partial swap with price limit - may not consume full input amount amountIn, amountOut := ExactInSingleSwapRoute( cross(cur), "gno.land/r/gnoswap/gns.GNS", // input token "gno.land/r/gnoland/wugnot.wugnot", // output token "1000000", // max amount to swap "gno.land/r/gnoswap/gns.GNS:gno.land/r/gnoland/wugnot.wugnot:3000", // single route "950000", // min output "1000000000000000000", // sqrtPriceLimitX96 (price limit) deadline, "", ) // If the price limit is reached, only a partial amount is swapped. For exact-in // this can consume less input; exact-out can deliver less than its target. // amountOutMin or amountInMax remains enforced, respectively. ``` ## Important Developer Notes ### Common Integration Pitfalls 1. **Native Token Assumptions**: Passing `"ugnot"` to router swap functions will fail because router entrypoints reject native-coin handling. 2. **Route vs Token Identifier Confusion**: Using `"ugnot"` in route strings instead of `"gno.land/r/gnoland/wugnot.wugnot"` will cause transactions to fail since no pools exist for the `"ugnot"` identifier. 3. **Wrong Token Path**: - Use `gno.land/r/gnoland/wugnot.wugnot` when swapping wrapped GNOT - Do not pass native `ugnot` to router swap functions - Route strings must stay in swap-direction order and use token contract paths ### Frontend Integration Checklist - [ ] Implement WUGNOT approval before wrapped-GNOT swaps - [ ] Use token keys (`pkgPath.SYMBOL`) such as `"gno.land/r/gnoland/wugnot.wugnot"` for both parameters and routes - [ ] Test both partial and full swap scenarios - [ ] Implement proper error handling for failed approvals Both single-hop functions support partial execution when `sqrtPriceLimitX96` is nonzero: - Exact-in may consume less than the specified input amount - Exact-out may deliver less than the requested post-fee output - The relevant amount limit (`amountOutMin` or `amountInMax`) still applies - Remaining input tokens stay with the user because the router uses token contract transfers - A zero limit uses the global tick-math boundary and preserves full exact semantics ## Security - Path validation checks syntax, endpoints, hop continuity, and pool existence; it does not reject circular routes - Deadline prevents stale live transactions - Slippage limits protect against unfavorable execution - The router fee rate is configurable; the current rate is fixed during one execution - WUGNOT approval requirement prevents unauthorized token transfers
  4. #4assert.gno
  5. #5package router import ( "errors" "strings" "time" ufmt "gno.land/p/nt/ufmt/v0" u256 "gno.land/p/gnoswap/uint256/v1" "gno.land/r/gnoswap/pool" "gno.land/r/gnoswap/common" ) // assertIsNotExpired ensures the transaction deadline has not passed. func assertIsNotExpired(deadline int64) { now := time.Now().Unix() if now > deadline { panic(makeErrorWithDetails( errExpired, ufmt.Sprintf("transaction too old, now(%d) > deadline(%d)", now, deadline), )) } } func assertSingleSwap(p *SingleSwapParams) { if p.tokenIn == p.tokenOut { panic(errors.New(errSameTokenSwap)) } common.MustRegistered(p.tokenIn, p.tokenOut) } func assertIsValidSqrtPriceLimitX96(sqrtPriceLimitX96 string) { if sqrtPriceLimitX96 == "" { panic(makeErrorWithDetails( errInvalidInput, ufmt.Sprintf("invalid sqrtPriceLimitX96: %s", sqrtPriceLimitX96), )) } _, err := u256.FromDecimal(sqrtPriceLimitX96) if err != nil { panic(makeErrorWithDetails( errInvalidInput, ufmt.Sprintf("invalid sqrtPriceLimitX96: %s", sqrtPriceLimitX96), )) } } func assertIsValidSingleSwapRouteArrPath(routePaths, inputToken, outputToken string) { if routePaths == "" { panic(makeErrorWithDetails( errInvalidInput, ufmt.Sprintf("invalid route: %s", routePaths), )) } if strings.Count(routePaths, ",") > 0 { panic(makeErrorWithDetails( errInvalidInput, ufmt.Sprintf("invalid routePaths: %s", routePaths), )) } if strings.Count(routePaths, POOL_SEPARATOR) > 0 { panic(makeErrorWithDetails( errInvalidInput, ufmt.Sprintf("invalid routePaths: %s", routePaths), )) } assertIsValidRoutePaths(routePaths, inputToken, outputToken) } func assertIsValidRoutePaths(routePaths, inputToken, outputToken string) { err := validateRoutePaths(routePaths, inputToken, outputToken) if err != nil { panic(err) } } func assertIsExistsPools(routePathArr string) { poolPaths, err := parsePoolPathsByRoutePathArr(routePathArr) if err != nil { panic(err) } for _, poolPath := range poolPaths { if !pool.ExistsPoolPath(poolPath) { panic(makeErrorWithDetails( errInvalidInput, ufmt.Sprintf("pool does not exist: %s", poolPath), )) } } } func assertIsRouterImplementation(caller address) { if caller != routerImplAddr { panic(makeErrorWithDetails( errUnAuthorizedCaller, ufmt.Sprintf("caller %s is not router implementation(%s)", caller, routerImplAddr), )) } } // assertHopFullyConsumed reverts when an exact-in hop consumed less input than specified. func assertHopFullyConsumed(hopIndex int, specifiedAmount, consumedAmount int64) { err := validateHopFullyConsumed(hopIndex, specifiedAmount, consumedAmount) if err != nil { panic(err) } } func validateHopFullyConsumed(hopIndex int, specifiedAmount, consumedAmount int64) error { if consumedAmount != specifiedAmount { return makeErrorWithDetails( errInsufficientLiquidity, ufmt.Sprintf("partial fill on hop(%d): requested=%d, consumed=%d", hopIndex, specifiedAmount, consumedAmount), ) } return nil }
  6. #6base.gno
  7. #7package router import ( "errors" "strings" gnsmath "gno.land/p/gnoswap/gnsmath/v1" prbac "gno.land/p/gnoswap/rbac/v1" u256 "gno.land/p/gnoswap/uint256/v1" ufmt "gno.land/p/nt/ufmt/v0" "gno.land/r/gnoswap/access/v1" ) const ( SINGLE_HOP_ROUTE int = 1 ) // swap can be done by multiple pools // to separate each pool, we use POOL_SEPARATOR const ( POOL_SEPARATOR = "*POOL*" ) type RouterOperation interface { // Validate checks operation parameters before processing. // // Returns: // - err: nil when operation input is valid; validation error otherwise Validate() error // Process executes the operation in the propagated realm context. // // Parameters: // - _: leading operation discriminator; callers pass 0 // - rlm: propagated current realm context used for pool interaction // // Returns: // - result: swap amounts and route metadata, or nil when processing fails // - err: nil on success; execution error otherwise Process(_ int, rlm realm) (*SwapResult, error) } // executeSwapOperation validates and processes a swap operation. func executeSwapOperation(_ int, rlm realm, op RouterOperation) (*SwapResult, error) { if err := op.Validate(); err != nil { return nil, err } result, err := op.Process(0, rlm) if err != nil { return nil, err } return result, nil } type BaseSwapParams struct { InputToken string OutputToken string RouteArr string QuoteArr string SqrtPriceLimitX96 *u256.Uint Deadline int64 } // common swap operation type baseSwapOperation struct { sqrtPriceLimitX96 *u256.Uint routes []string quotes []string amountSpecified int64 } // processRoutes processes all swap routes and returns total amounts. func (op *baseSwapOperation) processRoutes(_ int, rlm realm, r *routerV1, swapType SwapType) (int64, int64, error) { resultAmountIn, resultAmountOut := int64(0), int64(0) remainRequestAmount := op.amountSpecified for i, route := range op.routes { toSwapAmount := int64(0) // if it's the last route, use the remaining amount isLastRoute := i == len(op.routes)-1 if !isLastRoute { // calculate the amount to swap for this route swapAmount, err := calculateSwapAmountByQuote(op.amountSpecified, op.quotes[i]) if err != nil { return 0, 0, err } // update the remaining amount remainRequestAmount = gnsmath.SafeSubInt64(remainRequestAmount, swapAmount) toSwapAmount = swapAmount } else { toSwapAmount = remainRequestAmount } amountIn, amountOut, err := op.processRoute(0, rlm, r, route, toSwapAmount, swapType) if err != nil { return 0, 0, err } resultAmountIn = gnsmath.SafeAddInt64(resultAmountIn, amountIn) resultAmountOut = gnsmath.SafeAddInt64(resultAmountOut, amountOut) } return resultAmountIn, resultAmountOut, nil } // processRoute processes a single route with specified swap amount. func (op *baseSwapOperation) processRoute( _ int, rlm realm, r *routerV1, route string, toSwap int64, swapType SwapType, ) (amountIn, amountOut int64, err error) { numHops := strings.Count(route, POOL_SEPARATOR) + 1 assertHopsInRange(numHops) switch numHops { case SINGLE_HOP_ROUTE: amountIn, amountOut = r.handleSingleSwap(0, rlm, route, toSwap, op.sqrtPriceLimitX96) default: amountIn, amountOut = r.handleMultiSwap(0, rlm, swapType, route, numHops, toSwap) } return amountIn, amountOut, nil } // handleSingleSwap executes a single-hop swap with the specified amount. func (r *routerV1) handleSingleSwap(_ int, rlm realm, route string, amountSpecified int64, sqrtPriceLimitX96 *u256.Uint) (int64, int64) { input, output, fee := getDataForSinglePath(route) singleParams := SingleSwapParams{ tokenIn: input, tokenOut: output, fee: fee, amountSpecified: amountSpecified, sqrtPriceLimitX96: sqrtPriceLimitX96, } return r.singleSwap(0, rlm, &singleParams) } // handleMultiSwap processes multi-hop swaps across multiple pools. func (r *routerV1) handleMultiSwap( _ int, rlm realm, swapType SwapType, route string, numHops int, amountSpecified int64, ) (int64, int64) { recipient := access.MustGetAddress(prbac.ROLE_ROUTER.String()) switch swapType { case ExactIn: input, output, fee := getDataForMultiPath(route, 0) // first data sp := newSwapParams(input, output, fee, recipient, amountSpecified) return r.multiSwap(0, rlm, *sp, numHops, route) case ExactOut: input, output, fee := getDataForMultiPath(route, numHops-1) // last data sp := newSwapParams(input, output, fee, recipient, amountSpecified) return r.multiSwapNegative(0, rlm, *sp, numHops, route) default: panic(errors.New(errInvalidSwapType)) } } // SwapRouteParams contains all parameters needed for swap route execution type SwapRouteParams struct { inputToken string outputToken string routeArr string quoteArr string deadline int64 typ SwapType exactAmount int64 // amountIn for ExactIn, amountOut for ExactOut limitAmount int64 // amountOutMin for ExactIn, amountInMax for ExactOut sqrtPriceLimitX96 *u256.Uint // if sqrtPriceLimitX96 is zero string, it will be set to MIN_PRICE or MAX_PRICE } // ExactAmount returns the requested exact input or output amount. // // Returns: // - amount: exactAmount configured for the route func (p *SwapRouteParams) ExactAmount() int64 { return p.exactAmount } // ExpectedExactAmountByFee returns the amount needed by pool execution after router-fee adjustment. // // Parameters: // - feeBps: router fee in basis points // // Returns: // - amount: exact input amount for ExactIn, or gross pool amount for ExactOut func (p *SwapRouteParams) ExpectedExactAmountByFee(feeBps uint64) int64 { if p.typ == ExactIn { return p.exactAmount } return calculateExactOutWithRouterFee(p.exactAmount, feeBps) } // SwapCount counts pool hops across all comma-separated routes. // // Returns: // - count: total number of pool swaps represented by routeArr func (p *SwapRouteParams) SwapCount() int64 { swapCount := int64(0) for _, route := range strings.Split(p.routeArr, ",") { swapCount += int64(strings.Count(route, POOL_SEPARATOR) + 1) } return swapCount } func buildRouteEventAttrs(routeArr string) []string { routes := strings.Split(routeArr, ",") routeEventAttrs := make([]string, 0, len(routes)*2) for index, route := range routes { routeEventAttrs = append(routeEventAttrs, ufmt.Sprintf("routes[%d]", index)) routeEventAttrs = append(routeEventAttrs, route) } return routeEventAttrs } // IsSetSqrtPriceLimitX96 reports whether a nonzero caller price limit is configured. // // Returns: // - set: true when sqrtPriceLimitX96 is non-nil and nonzero func (p *SwapRouteParams) IsSetSqrtPriceLimitX96() bool { return p.sqrtPriceLimitX96 != nil && !p.sqrtPriceLimitX96.IsZero() } // createSwapOperation creates the appropriate swap operation based on swap type. func createSwapOperation(r *routerV1, params SwapRouteParams) (RouterOperation, error) { baseParams := BaseSwapParams{ InputToken: params.inputToken, OutputToken: params.outputToken, RouteArr: params.routeArr, QuoteArr: params.quoteArr, SqrtPriceLimitX96: params.sqrtPriceLimitX96, Deadline: params.deadline, } switch params.typ { case ExactIn: pp := NewExactInParams(baseParams, params.ExactAmount(), params.limitAmount) return NewExactInSwapOperation(r, pp), nil case ExactOut: routerFee := r.store.GetSwapFee() pp := NewExactOutParams(baseParams, params.ExpectedExactAmountByFee(routerFee), params.limitAmount) return NewExactOutSwapOperation(r, pp), nil default: msg := addDetailToError(errInvalidSwapType, "unknown swap type") return nil, errors.New(msg) } } // commonSwapRoute handles the common logic for both ExactIn and ExactOut swaps. func (r *routerV1) commonSwapRoute(_ int, rlm realm, params SwapRouteParams) (int64, int64, error) { op, err := createSwapOperation(r, params) if err != nil { return 0, 0, err } result, err := executeSwapOperation(0, rlm, op) if err != nil { msg := addDetailToError( errInvalidInput, ufmt.Sprintf("invalid %s SwapOperation: %s", params.typ.String(), err.Error()), ) return 0, 0, errors.New(msg) } inputAmount, outputAmount := r.finalizeSwap( 0, rlm, params.inputToken, params.outputToken, result.AmountIn, result.AmountOut, params.typ, params.limitAmount, params.ExactAmount(), params.SwapCount(), params.IsSetSqrtPriceLimitX96(), ) return inputAmount, outputAmount, nil }
  8. #8consts.gno
  9. #9package router var routerImplAddr address func init(cur realm) { routerImplAddr = cur.Address() }
  10. #10doc.gno
  11. #11// package router handles token swaps through GnoSwap liquidity pools. // // The router provides user-facing swap functions with slippage protection, // multi-hop routing, and both exact-input and exact-output swap modes. // // Live exact-input calls enforce a minimum output and deadline. Live // exact-output calls enforce a maximum input and deadline. DrySwapRoute is // read-only and has no deadline or transfer. package router
  12. #12errors.gno
  13. #13package router import ( ufmt "gno.land/p/nt/ufmt/v0" ) const ( errSlippage = "[GNOSWAP-ROUTER-001] slippage check failed" errInvalidRoutesAndQuotes = "[GNOSWAP-ROUTER-002] invalid routes and quotes" errExpired = "[GNOSWAP-ROUTER-003] transaction expired" errInvalidInput = "[GNOSWAP-ROUTER-004] invalid input data" errInvalidPoolFeeTier = "[GNOSWAP-ROUTER-005] invalid pool fee tier" errInvalidSwapFee = "[GNOSWAP-ROUTER-006] invalid swap fee" errInvalidSwapType = "[GNOSWAP-ROUTER-007] invalid swap type" errInvalidPoolPath = "[GNOSWAP-ROUTER-008] invalid pool path" errUnAuthorizedCaller = "[GNOSWAP-ROUTER-009] unauthorized caller" errHopsOutOfRange = "[GNOSWAP-ROUTER-010] number of hops must be 1~3" errSameTokenSwap = "[GNOSWAP-ROUTER-011] cannot swap same token" errInvalidRoutePath = "[GNOSWAP-ROUTER-013] invalid route path" errInvalidRouteFirstToken = "[GNOSWAP-ROUTER-014] invalid route first token" errInvalidRouteLastToken = "[GNOSWAP-ROUTER-015] invalid route last token" errInvalidSwapAmount = "[GNOSWAP-ROUTER-016] invalid swap amount" errRouteHopDisconnected = "[GNOSWAP-ROUTER-017] route hop disconnected" errInsufficientBalance = "[GNOSWAP-ROUTER-018] insufficient balance for swap" errSpoofedRealm = "[GNOSWAP-ROUTER-019] rlm does not match the current crossing frame" errInsufficientLiquidity = "[GNOSWAP-ROUTER-020] insufficient liquidity for swap" ) // addDetailToError adds detail to an error message. func addDetailToError(message string, detail string) string { finalErr := ufmt.Errorf("%s || %s", message, detail) return finalErr.Error() } // makeErrorWithDetails creates an error with additional context. func makeErrorWithDetails(message string, detail string) error { return ufmt.Errorf("%s || %s", message, detail) }
  14. #14exact_in.gno
  15. #15package router import ( "chain" "gno.land/r/gnoswap/access/v1" ufmt "gno.land/p/nt/ufmt/v0" u256 "gno.land/p/gnoswap/uint256/v1" "gno.land/p/gnoswap/utils/v1" "gno.land/r/gnoswap/common" "gno.land/r/gnoswap/emission" "gno.land/r/gnoswap/halt/v1" "gno.land/r/gnoswap/referral/v1" ) // ExactInSwapRoute swaps an exact amount of input tokens for output tokens. // // Executes multi-hop swaps through specified route. // Supports splitting across multiple paths for price optimization. // Applies slippage protection via minimum output amount. // // Parameters: // - _: leading implementation discriminator; callers pass 0 // - rlm: propagated current realm context; this implementation validates and forwards it // - inputToken: Input token contract path // - outputToken: Output token contract path // - amountIn: Exact input amount to swap // - routeArr: Swap route (max 3 hops per path, multiple paths separated by comma) // - quoteArr: Split percentages "70,30" (must sum to 100) // - amountOutMin: Minimum acceptable output (slippage protection) // - deadline: Unix timestamp for expiration // - referrer: Optional referral address // // Route format: // - Single-hop: "TOKEN0:TOKEN1:FEE" // - Multi-hop: "TOKEN0:TOKEN1:FEE*POOL*TOKEN1:TOKEN2:FEE" (use *POOL* separator) // - Multi-path: "route1,route2" (use comma separator between different routes) // // Returns: // - amountIn: Actual input consumed // - amountOut: Actual output received after the router fee // // Reverts if output < amountOutMin or deadline passed. func (r *routerV1) ExactInSwapRoute( _ int, rlm realm, inputToken string, outputToken string, amountIn string, routeArr string, quoteArr string, amountOutMin string, deadline int64, referrer string, ) (string, string) { access.AssertIsRlmCurrent(0, rlm) halt.AssertIsNotHaltedRouter() common.AssertIsNotHandleNativeCoin() assertIsValidRoutePaths(routeArr, inputToken, outputToken) assertIsNotExpired(deadline) assertIsExistsPools(routeArr) emission.MintAndDistributeGns(cross(rlm)) params := SwapRouteParams{ inputToken: inputToken, outputToken: outputToken, routeArr: routeArr, quoteArr: quoteArr, deadline: deadline, typ: ExactIn, exactAmount: utils.SafeParseInt64(amountIn), limitAmount: utils.SafeParseInt64(amountOutMin), sqrtPriceLimitX96: u256.Zero(), // multi-hop swap is not allowed to set sqrtPriceLimitX96 } inputAmount, outputAmount := r.exactInSwapRoute(0, rlm, params, referrer) return utils.FormatInt(inputAmount), utils.FormatInt(outputAmount) } // ExactInSingleSwapRoute swaps an exact amount of input tokens for output tokens through a single route. // // Executes a single-hop swap through a single specified route. // Allows price limit control via sqrtPriceLimitX96 parameter. // Applies slippage protection via minimum output amount. // // Parameters: // - _: leading implementation discriminator; callers pass 0 // - rlm: propagated current realm context; this implementation validates and forwards it // - inputToken: Input token contract path // - outputToken: Output token contract path // - amountIn: Exact input amount to swap // - routeArr: Single-hop route path // - amountOutMin: Minimum acceptable output (slippage protection) // - sqrtPriceLimitX96: Price limit for swap execution (0 for no limit) // - deadline: Unix timestamp for expiration // - referrer: Optional referral address // // Route format: // - Single-hop: "INPUT_TOKEN:OUTPUT_TOKEN:FEE" // // Returns: // - amountIn: Actual input consumed // - amountOut: Actual output received after the router fee // // With a nonzero price limit, the swap may stop at the limit after consuming // less than amountIn; amountOutMin and the deadline are still enforced. func (r *routerV1) ExactInSingleSwapRoute( _ int, rlm realm, inputToken string, outputToken string, amountIn string, routeArr string, amountOutMin string, sqrtPriceLimitX96 string, deadline int64, referrer string, ) (string, string) { access.AssertIsRlmCurrent(0, rlm) halt.AssertIsNotHaltedRouter() common.AssertIsNotHandleNativeCoin() assertIsValidSingleSwapRouteArrPath(routeArr, inputToken, outputToken) assertIsValidSqrtPriceLimitX96(sqrtPriceLimitX96) assertIsNotExpired(deadline) assertIsExistsPools(routeArr) emission.MintAndDistributeGns(cross(rlm)) params := SwapRouteParams{ inputToken: inputToken, outputToken: outputToken, routeArr: routeArr, quoteArr: "100", deadline: deadline, typ: ExactIn, exactAmount: utils.SafeParseInt64(amountIn), limitAmount: utils.SafeParseInt64(amountOutMin), sqrtPriceLimitX96: u256.MustFromDecimal(sqrtPriceLimitX96), // single swap is allowed to set sqrtPriceLimitX96 } inputAmount, outputAmount := r.exactInSwapRoute(0, rlm, params, referrer) return utils.FormatInt(inputAmount), utils.FormatInt(outputAmount) } // exactInSwapRoute executes the swap operation and handles token transfers and referral registration. // // Performs the actual swap operation using commonSwapRoute and handles: // - Safe token transfers to the caller // - Referral registration and tracking // - Event emission for swap completion // // Parameters: // - params: SwapRouteParams containing all swap configuration // - referrer: Referral address for registration // // Returns: // - inputAmount: Actual input amount consumed as string // - outputAmount: Actual output amount received as string // // Panics if swap execution fails or token transfer fails. func (r *routerV1) exactInSwapRoute( _ int, rlm realm, params SwapRouteParams, referrer string, ) (int64, int64) { inputAmount, outputAmount, err := r.commonSwapRoute(0, rlm, params) if err != nil { panic(err) } previousRealm := rlm.Previous() caller := previousRealm.Address() common.SafeGRC20Transfer(0, rlm, params.outputToken, caller, outputAmount) // handle referral registration actualReferrer := referral.TryRegister(cross(rlm), caller, referrer) eventAttrs := append([]string{ "prevAddr", caller.String(), "prevRealm", previousRealm.PkgPath(), "input", params.inputToken, "output", params.outputToken, "exactAmount", utils.FormatInt(params.exactAmount), "quote", params.quoteArr, "resultInputAmount", utils.FormatInt(inputAmount), "resultOutputAmount", utils.FormatInt(outputAmount), "referrer", actualReferrer, }, buildRouteEventAttrs(params.routeArr)...) chain.Emit( "ExactInSwap", eventAttrs..., ) return inputAmount, outputAmount } type ExactInSwapOperation struct { baseSwapOperation params ExactInParams router *routerV1 } // NewExactInSwapOperation creates an exact-input operation for router execution. // // Parameters: // - r: router implementation used to execute pool swaps // - pp: exact-input amount and route settings // // Returns: // - operation: initialized ExactInSwapOperation func NewExactInSwapOperation(r *routerV1, pp ExactInParams) *ExactInSwapOperation { return &ExactInSwapOperation{ router: r, params: pp, baseSwapOperation: baseSwapOperation{ sqrtPriceLimitX96: pp.SqrtPriceLimitX96, }, } } // Validate checks the exact-input amount and parses route/quote allocations. // // Returns: // - err: nil when amount, routes, and quotes are valid; validation error otherwise func (op *ExactInSwapOperation) Validate() error { amountIn := op.params.AmountIn if amountIn <= 0 { return ufmt.Errorf("invalid amountIn(%d), must be positive", amountIn) } // when `SwapType` is `ExactIn`, assign `amountSpecified` the `amountIn` // obtained from above. op.amountSpecified = amountIn routes, quotes, err := validateRoutesAndQuotes(op.params.RouteArr, op.params.QuoteArr) if err != nil { return err } op.routes = routes op.quotes = quotes return nil } // Process executes the validated exact-input operation in the propagated realm. // // Parameters: // - _: leading operation discriminator; callers pass 0 // - rlm: propagated current realm context used for pool swaps // // Returns: // - result: aggregate swap amounts and route metadata, or nil on processing error // - err: nil on success; pool or route execution error otherwise func (op *ExactInSwapOperation) Process(_ int, rlm realm) (*SwapResult, error) { resultAmountIn, resultAmountOut, err := op.processRoutes(0, rlm, op.router, ExactIn) if err != nil { return nil, err } return &SwapResult{ AmountIn: resultAmountIn, AmountOut: resultAmountOut, Routes: op.routes, Quotes: op.quotes, AmountSpecified: op.amountSpecified, }, nil }
  16. #16exact_out.gno
  17. #17package router import ( "chain" "gno.land/r/gnoswap/access/v1" ufmt "gno.land/p/nt/ufmt/v0" u256 "gno.land/p/gnoswap/uint256/v1" "gno.land/p/gnoswap/utils/v1" "gno.land/r/gnoswap/common" "gno.land/r/gnoswap/emission" "gno.land/r/gnoswap/halt/v1" "gno.land/r/gnoswap/referral/v1" ) // ExactOutSwapRoute swaps tokens for a requested post-router-fee output target. // // Executes a swap to receive the requested net output tokens. // Calculates the required gross pool output and input working backwards // through the route. // // Parameters: // - _: leading implementation discriminator; callers pass 0 // - rlm: propagated current realm context; this implementation validates and forwards it // - inputToken: Input token contract path // - outputToken: Output token contract path // - amountOut: Requested net output target // - routeArr: Swap routes separated by comma; each route may contain up to 3 hops joined by *POOL* // - quoteArr: Split percentages "70,30" (must sum to 100) // - amountInMax: Maximum input to spend (slippage protection) // - deadline: Unix timestamp for expiration // - referrer: Optional referral address // // Route calculation: // - Works backwards from output to input // - Each hop increases required input // - Multi-path aggregates total input // // Returns: // - amountIn: Actual input consumed // - amountOut: Net output delivered after the router fee // // Reverts if input > amountInMax or deadline passed. func (r *routerV1) ExactOutSwapRoute( _ int, rlm realm, inputToken string, outputToken string, amountOut string, routeArr string, quoteArr string, amountInMax string, deadline int64, referrer string, ) (string, string) { access.AssertIsRlmCurrent(0, rlm) halt.AssertIsNotHaltedRouter() common.AssertIsNotHandleNativeCoin() assertIsValidRoutePaths(routeArr, inputToken, outputToken) assertIsNotExpired(deadline) assertIsExistsPools(routeArr) emission.MintAndDistributeGns(cross(rlm)) params := SwapRouteParams{ inputToken: inputToken, outputToken: outputToken, routeArr: routeArr, quoteArr: quoteArr, deadline: deadline, typ: ExactOut, exactAmount: utils.SafeParseInt64(amountOut), limitAmount: utils.SafeParseInt64(amountInMax), sqrtPriceLimitX96: u256.Zero(), // multi-hop swap is not allowed to set sqrtPriceLimitX96 } inputAmount, outputAmount := r.exactOutSwapRoute(0, rlm, params, referrer) return utils.FormatInt(inputAmount), utils.FormatInt(outputAmount) } // ExactOutSingleSwapRoute swaps tokens for a requested post-router-fee output // target through a single route. // // Executes a single-hop swap through a single specified route. // Allows price limit control via sqrtPriceLimitX96. // Applies slippage protection via maximum input amount. // // Parameters: // - _: leading implementation discriminator; callers pass 0 // - rlm: propagated current realm context; this implementation validates and forwards it // - inputToken: Input token contract path // - outputToken: Output token contract path // - amountOut: Requested net output target // - routeArr: Single-hop route path // - amountInMax: Maximum input to spend (slippage protection) // - sqrtPriceLimitX96: Price limit for swap execution (0 for no limit) // - deadline: Unix timestamp for expiration // - referrer: Optional referral address // // Route format: // - Single-hop: "INPUT_TOKEN:OUTPUT_TOKEN:FEE" // // Returns: // - amountIn: Actual input consumed // - amountOut: Net output delivered after the router fee // // With a nonzero price limit, the swap may stop at the limit and deliver less // than amountOut; amountInMax and the deadline are still enforced. func (r *routerV1) ExactOutSingleSwapRoute( _ int, rlm realm, inputToken string, outputToken string, amountOut string, routeArr string, amountInMax string, sqrtPriceLimitX96 string, deadline int64, referrer string, ) (string, string) { access.AssertIsRlmCurrent(0, rlm) halt.AssertIsNotHaltedRouter() common.AssertIsNotHandleNativeCoin() assertIsValidSingleSwapRouteArrPath(routeArr, inputToken, outputToken) assertIsValidSqrtPriceLimitX96(sqrtPriceLimitX96) assertIsNotExpired(deadline) assertIsExistsPools(routeArr) emission.MintAndDistributeGns(cross(rlm)) params := SwapRouteParams{ inputToken: inputToken, outputToken: outputToken, routeArr: routeArr, quoteArr: "100", deadline: deadline, typ: ExactOut, exactAmount: utils.SafeParseInt64(amountOut), limitAmount: utils.SafeParseInt64(amountInMax), sqrtPriceLimitX96: u256.MustFromDecimal(sqrtPriceLimitX96), // single swap is allowed to set sqrtPriceLimitX96 } inputAmount, outputAmount := r.exactOutSwapRoute(0, rlm, params, referrer) return utils.FormatInt(inputAmount), utils.FormatInt(outputAmount) } // exactOutSwapRoute executes the swap operation and handles token transfers and referral registration. // // Performs the actual swap operation using commonSwapRoute and handles: // - Safe token transfers to the caller // - Referral registration and tracking // - Event emission for swap completion // // Parameters: // - params: SwapRouteParams containing all swap configuration // - referrer: Referral address for registration // // Returns: // - inputAmount: Actual input amount consumed as string // - outputAmount: Actual output amount received as string // // Panics if swap execution fails or token transfer fails. func (r *routerV1) exactOutSwapRoute( _ int, rlm realm, params SwapRouteParams, referrer string, ) (int64, int64) { inputAmount, outputAmount, err := r.commonSwapRoute(0, rlm, params) if err != nil { panic(err) } previousRealm := rlm.Previous() caller := previousRealm.Address() common.SafeGRC20Transfer(0, rlm, params.outputToken, caller, outputAmount) // handle referral registration actualReferrer := referral.TryRegister(cross(rlm), caller, referrer) eventAttrs := append([]string{ "prevAddr", caller.String(), "prevRealm", previousRealm.PkgPath(), "input", params.inputToken, "output", params.outputToken, "exactAmount", utils.FormatInt(params.exactAmount), "quote", params.quoteArr, "resultInputAmount", utils.FormatInt(inputAmount), "resultOutputAmount", utils.FormatInt(outputAmount), "referrer", actualReferrer, }, buildRouteEventAttrs(params.routeArr)...) chain.Emit( "ExactOutSwap", eventAttrs..., ) return inputAmount, outputAmount } // ExactOutSwapOperation handles swaps where the output amount is specified. type ExactOutSwapOperation struct { router *routerV1 baseSwapOperation params ExactOutParams } // NewExactOutSwapOperation creates an exact-output operation for router execution. // // Parameters: // - r: router implementation used to execute pool swaps // - pp: exact-output amount and route settings // // Returns: // - operation: initialized ExactOutSwapOperation func NewExactOutSwapOperation(r *routerV1, pp ExactOutParams) *ExactOutSwapOperation { return &ExactOutSwapOperation{ router: r, params: pp, baseSwapOperation: baseSwapOperation{ sqrtPriceLimitX96: pp.SqrtPriceLimitX96, }, } } // Validate checks the exact-output amount and parses route/quote allocations. // // Returns: // - err: nil when amount, routes, and quotes are valid; validation error otherwise func (op *ExactOutSwapOperation) Validate() error { amountOut := op.params.AmountOut if amountOut <= 0 { return ufmt.Errorf("invalid amountOut(%d), must be positive", amountOut) } // assign a signed reversed `amountOut` to `amountSpecified` // when it's an ExactOut op.amountSpecified = -amountOut routes, quotes, err := validateRoutesAndQuotes(op.params.RouteArr, op.params.QuoteArr) if err != nil { return err } op.routes = routes op.quotes = quotes return nil } // Process executes the validated exact-output operation in the propagated realm. // // Parameters: // - _: leading operation discriminator; callers pass 0 // - rlm: propagated current realm context used for pool swaps // // Returns: // - result: aggregate swap amounts and route metadata, or nil on processing error // - err: nil on success; pool or route execution error otherwise func (op *ExactOutSwapOperation) Process(_ int, rlm realm) (*SwapResult, error) { resultAmountIn, resultAmountOut, err := op.processRoutes(0, rlm, op.router, ExactOut) if err != nil { return nil, err } return &SwapResult{ AmountIn: resultAmountIn, AmountOut: resultAmountOut, Routes: op.routes, Quotes: op.quotes, AmountSpecified: op.amountSpecified, }, nil }
  18. #18gnomod.toml
  19. #19module = "gno.land/r/gnoswap/router/v1" gno = "0.9"
  20. #20init.gno
  21. #21package router import ( "gno.land/r/gnoswap/router" ) const ( defaultSwapFeeBPS = uint64(15) // 0.15% ) func init(cur realm) { registerRouterV1(cur) } func registerRouterV1(cur realm) { router.RegisterInitializer(cross(cur), func(_ int, rlm realm, routerStore router.IRouterStore) router.IRouter { err := initStoreData(0, rlm, routerStore) if err != nil { panic(err) } return NewRouterV1(routerStore) }) } func initStoreData(_ int, rlm realm, routerStore router.IRouterStore) error { if !routerStore.HasSwapFeeKey() { err := routerStore.SetSwapFee(0, rlm, defaultSwapFeeBPS) if err != nil { return err } } pendingProtocolFees := make(map[string]int64) if routerStore.HasPendingProtocolFeesKey() { pendingProtocolFees = routerStore.GetPendingProtocolFees() } if err := routerStore.SetPendingProtocolFees(0, rlm, pendingProtocolFees); err != nil { return err } return nil }
  22. #22instance.gno
  23. #23package router import ( "gno.land/r/gnoswap/router" ) type routerV1 struct { store router.IRouterStore } // NewRouterV1 creates a router v1 implementation backed by routerStore. // // Parameters: // - routerStore: storage implementation used for swap-fee and pending protocol-fee state // // Returns: // - router: initialized IRouter implementation backed by routerStore func NewRouterV1( routerStore router.IRouterStore, ) router.IRouter { instance := &routerV1{ store: routerStore, } return instance }
  24. #24protocol_fee_swap.gno

Result log

msg:0,success:true,log:,events:[]