UniversalRouter
The UniversalRouter is a single entry-point contract that routes swaps across all three protocol versions (V2.5, V3, and V4) using a command-based execution model. Instead of calling version-specific routers directly, callers encode a sequence of commands and their ABI-encoded inputs, then dispatch everything in one execute() call.
Deployment (Arc Testnet)
| Contract | Address |
|---|---|
| UniversalRouter | 0xC43cC6A1E0F6EB48Cd4131522C1C73B13f3Da0F1 |
| Permit2 | 0x4ce562F687d0Ced27b79Ba51d79B63BD978F7F48 |
Execute Interface
All interactions go through a single payable function:
function execute( bytes calldata commands, // packed command bytes, one byte per command bytes[] calldata inputs, // ABI-encoded args for each command uint256 deadline // unix timestamp after which the call reverts ) external payable
Each byte in commands maps to one entry in inputs. Commands are executed sequentially in the order they appear.
Command Reference
| Command | Byte | Description |
|---|---|---|
V3_SWAP_EXACT_IN | 0x00 | V3 exact-input swap |
V3_SWAP_EXACT_OUT | 0x01 | V3 exact-output swap |
SWEEP | 0x04 | Sweep a token balance to a recipient |
V2_SWAP_EXACT_IN | 0x08 | V2.5 exact-input swap |
V2_SWAP_EXACT_OUT | 0x09 | V2.5 exact-output swap |
PERMIT2_PERMIT | 0x0a | Submit a Permit2 signature |
WRAP_ETH | 0x0b | Wrap native token to WUSDC |
UNWRAP_WETH | 0x0c | Unwrap WUSDC to native token |
V4_SWAP | 0x10 | V4 swap (encodes inner V4 actions) |
V4 Inner Actions
The V4_SWAP command takes a nested actions bytes payload that encodes V4-specific operations from v4-periphery Actions.sol:
| Action | Byte | Description |
|---|---|---|
SWAP_EXACT_IN_SINGLE | 0x06 | Single-pool exact-input swap |
SWAP_EXACT_IN | 0x07 | Multi-hop exact-input swap |
SWAP_EXACT_OUT_SINGLE | 0x08 | Single-pool exact-output swap |
SETTLE | 0x0b | Pay input currency, direct or via Permit2 |
SETTLE_ALL | 0x0c | Pay full input amount from msg.sender via Permit2 |
TAKE | 0x0e | Receive output to an explicit recipient address |
TAKE_ALL | 0x0f | Receive full output to msg.sender |
Init Code Hashes
The UniversalRouter uses init code hashes to compute pool addresses without an on-chain lookup. The values for Arc Testnet are:
| Version | Init Code Hash |
|---|---|
| V2.5 | 0x0000000000000000000000000000000000000000000000000000000000000000Zero hash - ArcFlow uses factory registry lookup instead of CREATE2 derivation |
| V3 | 0x00d70cee9321995764cc09cacfa52eb59ee6477448e0b4ed650c9cccfe446b18 |
Usage Example: V3 Exact-Input Swap
import { encodePacked, encodeAbiParameters, parseUnits } from 'viem'
// Command: V3_SWAP_EXACT_IN (0x00)
const commands = '0x00'
// Encode the path: tokenIn -> fee -> tokenOut
const path = encodePacked(
['address', 'uint24', 'address'],
[tokenInAddress, 3000, tokenOutAddress] // 0.30% fee tier
)
// Encode the swap input params
const input = encodeAbiParameters(
[
{ type: 'address' }, // recipient
{ type: 'uint256' }, // amountIn
{ type: 'uint256' }, // amountOutMinimum
{ type: 'bytes' }, // path
{ type: 'bool' }, // payerIsUser (true = pull from msg.sender via Permit2)
],
[recipientAddress, parseUnits('1', 6), parseUnits('0.99', 6), path, true]
)
await walletClient.writeContract({
address: '0xC43cC6A1E0F6EB48Cd4131522C1C73B13f3Da0F1',
abi: UNIVERSAL_ROUTER_ABI,
functionName: 'execute',
args: [commands, [input], BigInt(Math.floor(Date.now() / 1000) + 300)],
})Usage Example: V4 Exact-Input Single Swap
// Command: V4_SWAP (0x10)
const commands = '0x10'
// Inner V4 actions: SWAP_EXACT_IN_SINGLE (0x06) + SETTLE_ALL (0x0c) + TAKE_ALL (0x0f)
const v4Actions = encodePacked(
['uint8', 'uint8', 'uint8'],
[0x06, 0x0c, 0x0f]
)
const swapParams = encodeAbiParameters(
[{
type: 'tuple',
components: [
{ name: 'poolKey', type: 'tuple', components: [
{ name: 'currency0', type: 'address' },
{ name: 'currency1', type: 'address' },
{ name: 'fee', type: 'uint24' },
{ name: 'tickSpacing', type: 'int24' },
{ name: 'hooks', type: 'address' },
]},
{ name: 'zeroForOne', type: 'bool' },
{ name: 'amountIn', type: 'uint128' },
{ name: 'sqrtPriceLimitX96', type: 'uint160' },
{ name: 'hookData', type: 'bytes' },
],
}],
[{ poolKey, zeroForOne: true, amountIn: parseUnits('1', 6), sqrtPriceLimitX96: 0n, hookData: '0x' }]
)
const settleParams = encodeAbiParameters(
[{ type: 'address' }, { type: 'uint256' }],
[currency0Address, parseUnits('1', 6)]
)
const takeParams = encodeAbiParameters(
[{ type: 'address' }, { type: 'uint256' }],
[currency1Address, 0n] // 0 = take all
)
const v4Input = encodeAbiParameters(
[{ type: 'bytes' }, { type: 'bytes[]' }],
[v4Actions, [swapParams, settleParams, takeParams]]
)
await walletClient.writeContract({
address: '0xC43cC6A1E0F6EB48Cd4131522C1C73B13f3Da0F1',
abi: UNIVERSAL_ROUTER_ABI,
functionName: 'execute',
args: [commands, [v4Input], BigInt(Math.floor(Date.now() / 1000) + 300)],
})Source Reference
The UniversalRouter configuration lives in src/config/universalRouter.ts in the DEX repository. It exports:
UNIVERSAL_ROUTER_ADDRESS- contract address (overridable viaNEXT_PUBLIC_UNIVERSAL_ROUTER_ADDRESS)PERMIT2_ADDRESS- shared Permit2 addressUR_COMMANDS- command byte constantsV4_ACTIONS- V4 inner action byte constantsUNIVERSAL_ROUTER_ABI- minimal ABI for theexecutefunctionV3_POOL_INIT_CODE_HASH- V3 pool init code hash for Arc Testnet
UniversalRouter/script/deployParameters/DeployArcTestnet.s.sol and the deployed addresses are recorded in UniversalRouter/deploy-addresses/arc-testnet.json.