# 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.
## Gnoweb
The root `Render("")` delegates to the active implementation. It exposes separate
status and read-only swap-fee sections. Native Gnoweb execution forms show
one user-facing swap action per page:
- `""`: `ExactInSingleSwapRoute`
- `"swap/exact-in"`: `ExactInSwapRoute`
- `"swap/exact-out-single"`: `ExactOutSingleSwapRoute`
- `"swap/exact-out"`: `ExactOutSwapRoute`
Input placeholders show token-key, route, and percentage examples without
separate explanatory paragraphs. Token amounts are integer base units.
Set `amountOutMin` or `amountInMax` for slippage
protection, use a future Unix `deadline`, and set multi-route `quoteArr`
percentages to sum to 100. Approve each input GRC20 token for the router realm
before swapping. The single-route price limit accepts `sqrtPriceLimitX96` as a
base-10 Q64.96 square-root price; `0` disables it.
No privileged fee-setting form, guidance route, or navigation link is exposed.
`SetSwapFee` itself is unchanged. No internal callback form is exposed.
Unsupported render paths, including `"fee"`, return `404`.
## 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.
#### Example with Wrapped GNOT
Examples in this document 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.
```go
// 1. Approve WUGNOT spending for the router
wugnot.Approve(cross(cur), routerAddress, 1000000)
// 2. Call swap function with wrapped GNOT paths
amountIn, amountOut := ExactInSwapRoute(
cross(cur),
"gno.land/r/gnoland/wugnot.wugnot", // input token
"gno.land/r/gnoswap/gns.GNS", // output token
"1000000", // amount in wrapped token units
"gno.land/r/gnoland/wugnot.wugnot:gno.land/r/gnoswap/gns.GNS:3000",
"100", // 100% through route
"950000", // min output
time.Now().Unix() + 300, // deadline
"", // no referrer
)
```
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
### 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
DrySwapRoute(inputToken string, outputToken string, specifiedAmount string, swapTypeStr string, strRouteArr string, quoteArr string, tokenAmountLimit string) (string, string, interface {Error func() string})
ExactInSingleSwapRoute(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, inputToken string, outputToken string, amountIn string, routeArr string, amountOutMin string, sqrtPriceLimitX96 string, deadline int64, referrer string) (string, string)
ExactInSwapRoute(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, inputToken string, outputToken string, amountIn string, routeArr string, quoteArr string, amountOutMin string, deadline int64, referrer string) (string, string)
ExactOutSingleSwapRoute(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, inputToken string, outputToken string, amountOut string, routeArr string, amountInMax string, sqrtPriceLimitX96 string, deadline int64, referrer string) (string, string)
ExactOutSwapRoute(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, inputToken string, outputToken string, amountOut string, routeArr string, quoteArr string, amountInMax string, deadline int64, referrer string) (string, string)
GetImplementationPackagePath() string
GetPendingProtocolFees() map[string]int64
GetSwapFee() uint64
NewRouterStore(kvStore interface {AddAuthorizedCaller func(int, .uverse.realm, .uverse.address, gno.land/p/gnoswap/store/v1.Permission) .uverse.error; Delete func(int, .uverse.realm, string) .uverse.error; Get func(string) (interface {}, .uverse.error); GetAddress func(string) (.uverse.address, .uverse.error); GetAllKeys func() ([]string, .uverse.error); GetAuthorizedCallers func() (map[.uverse.address]gno.land/p/gnoswap/store/v1.Permission, .uverse.error); GetBPTree func(string) (*gno.land/p/nt/bptree/v0.BPTree, .uverse.error); GetBool func(string) (bool, .uverse.error); GetDomainAddress func() .uverse.address; GetInt64 func(string) (int64, .uverse.error); GetString func(string) (string, .uverse.error); GetUint64 func(string) (uint64, .uverse.error); Has func(string) bool; IsWriteAuthorized func(.uverse.address) bool; RemoveAuthorizedCaller func(int, .uverse.realm, .uverse.address) .uverse.error; Set func(int, .uverse.realm, string, interface {}) .uverse.error; UpdateAuthorizedCaller func(int, .uverse.realm, .uverse.address, gno.land/p/gnoswap/store/v1.Permission) .uverse.error}) interface {GetPendingProtocolFee func(string) int64; GetPendingProtocolFees func() map[string]int64; GetSwapFee func() uint64; HasPendingProtocolFeesKey func() bool; HasSwapFeeKey func() bool; RemovePendingProtocolFee func(int, .uverse.realm, string) .uverse.error; SetPendingProtocolFee func(int, .uverse.realm, string, int64) .uverse.error; SetPendingProtocolFees func(int, .uverse.realm, map[string]int64) .uverse.error; SetSwapFee func(int, .uverse.realm, uint64) .uverse.error}
RegisterInitializer(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, initializer func(int, .uverse.realm, gno.land/r/gnoswap/router.IRouterStore) gno.land/r/gnoswap/router.IRouter)
Render(path string) string
SetSwapFee(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, fee uint64)
SwapCallback(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, token0Path string, token1Path string, amount0Delta int64, amount1Delta int64, payer string) interface {Error func() string}
UpgradeImpl(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, packagePath string)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.