UnitFlow LogoUnitFlow Docs

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)

ContractAddress
UniversalRouter0xC43cC6A1E0F6EB48Cd4131522C1C73B13f3Da0F1
Permit20x4ce562F687d0Ced27b79Ba51d79B63BD978F7F48
ℹ️
Permit2 is shared across V3, V4, and the UniversalRouter. A single Permit2 approval covers all three.

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

CommandByteDescription
V3_SWAP_EXACT_IN0x00V3 exact-input swap
V3_SWAP_EXACT_OUT0x01V3 exact-output swap
SWEEP0x04Sweep a token balance to a recipient
V2_SWAP_EXACT_IN0x08V2.5 exact-input swap
V2_SWAP_EXACT_OUT0x09V2.5 exact-output swap
PERMIT2_PERMIT0x0aSubmit a Permit2 signature
WRAP_ETH0x0bWrap native token to WUSDC
UNWRAP_WETH0x0cUnwrap WUSDC to native token
V4_SWAP0x10V4 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:

ActionByteDescription
SWAP_EXACT_IN_SINGLE0x06Single-pool exact-input swap
SWAP_EXACT_IN0x07Multi-hop exact-input swap
SWAP_EXACT_OUT_SINGLE0x08Single-pool exact-output swap
SETTLE0x0bPay input currency, direct or via Permit2
SETTLE_ALL0x0cPay full input amount from msg.sender via Permit2
TAKE0x0eReceive output to an explicit recipient address
TAKE_ALL0x0fReceive 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:

VersionInit Code Hash
V2.50x0000000000000000000000000000000000000000000000000000000000000000
Zero hash - ArcFlow uses factory registry lookup instead of CREATE2 derivation
V30x00d70cee9321995764cc09cacfa52eb59ee6477448e0b4ed650c9cccfe446b18

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 via NEXT_PUBLIC_UNIVERSAL_ROUTER_ADDRESS)
  • PERMIT2_ADDRESS - shared Permit2 address
  • UR_COMMANDS - command byte constants
  • V4_ACTIONS - V4 inner action byte constants
  • UNIVERSAL_ROUTER_ABI - minimal ABI for the execute function
  • V3_POOL_INIT_CODE_HASH - V3 pool init code hash for Arc Testnet
ℹ️
Deployment parameters are defined in UniversalRouter/script/deployParameters/DeployArcTestnet.s.sol and the deployed addresses are recorded in UniversalRouter/deploy-addresses/arc-testnet.json.