Skip to main content
Helpful?

Introduction to Universal Router for Uniswap v4 Swaps

Uniswap v4 introduces a new architecture where all pools are managed by a single PoolManager contract. While the underlying architecture uses a callback system for swaps, developers can still use the Universal Router to execute swaps on v4 pools, just as you would for v2 or v3.

What is the Universal Router?​

The Universal Router is a flexible, gas-efficient contract designed to execute complex swap operations across various protocols, including Uniswap v4. It serves as an intermediary between users and the Uniswap v4 PoolManager, handling the intricacies of swap execution.

While it’s technically possible to interact directly with the PoolManager contract for swaps, this approach is generally not recommended due to its complexity and potential inefficiencies. The Universal Router is designed to abstract away these complexities, providing a more straightforward and efficient method for executing swaps on v4 pools.

UniversalRouter command encoding​

The Universal Router uses a unique encoding system for its commands and inputs, which is crucial to understand when configuring it for v4 swaps.

When calling UniversalRouter.execute(), you provide two main parameters:

  1. bytes commands: A string of bytes where each byte represents a single command to be executed.
  2. bytes[] inputs: An array of byte strings, each containing the encoded parameters for its corresponding command.

The commands[i] byte corresponds to the inputs[i] parameters, allowing for a series of operations to be defined and executed in sequence.

Each command is encoded as a single byte (bytes1) with a specific structure:

0 1 2 3 4 5 6 7
β”Œβ”€β”¬β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚fβ”‚r| command β”‚
β””β”€β”΄β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • The first bit (f) is a flag that determines whether the command is allowed to revert without causing the entire transaction to fail. This enables partial execution of complex transactions.
  • The second bit (r) is reserved for future use, providing flexibility for potential upgrades.
  • The remaining 6 bits represent the specific command to be executed.

Configuring Universal Router for Uniswap v4 Swaps

Use Cases​

Developers might need to configure the Universal Router for swapping on Uniswap v4 pools in several scenarios:

  1. Building a DEX aggregator: If you’re creating a platform that finds the best rates across multiple DEXes, you’ll want to include Uniswap v4 pools in your options.
  2. Developing a trading bot: Automated trading strategies often require the ability to execute swaps programmatically across various pools and versions.
  3. Creating a Dapp: Many DeFi applications (lending platforms, yield aggregators, etc.) need to perform token swaps as part of their core functionality.

This guide focuses on how to interact with Universal Router from an on-chain contract.

Step 1: Set Up the Project​

First, we need to set up our project and install the necessary dependencies.

forge install uniswap/v4-core
forge install uniswap/v4-periphery
forge install uniswap/permit2
forge install uniswap/universal-router
forge install OpenZeppelin/openzeppelin-contracts

In the remappings.txt, add the following:

@uniswap/v4-core/=lib/v4-core/
@uniswap/v4-periphery/=lib/v4-periphery/
@uniswap/permit2/=lib/permit2/
@uniswap/universal-router/=lib/universal-router/
@openzeppelin/contracts/=lib/openzeppelin-contracts/
[...]

We’ll create a new Solidity contract for our example.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import { UniversalRouter } from "@uniswap/universal-router/contracts/UniversalRouter.sol";
import { Commands } from "@uniswap/universal-router/contracts/libraries/Commands.sol";
import { IPoolManager } from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol";
import { IV4Router } from "@uniswap/v4-periphery/contracts/interfaces/IV4Router.sol";
import { Actions } from "@uniswap/v4-periphery/contracts/libraries/Actions.sol";
import { IPermit2 } from "@uniswap/permit2/contracts/interfaces/IPermit2.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract Example {
using StateLibrary for IPoolManager;

UniversalRouter public immutable router;
IPoolManager public immutable poolManager;
IPermit2 public immutable permit2;

constructor(address _router, address _poolManager, address _permit2) {
router = UniversalRouter(_router);
poolManager = IPoolManager(_poolManager);
permit2 = IPermit2(_permit2);
}

// We'll add more functions here
}

In this step, we’re importing the necessary contracts and interfaces:

  • UniversalRouter: This will be our main interface for executing swaps. It provides a flexible way to interact with various Uniswap versions and other protocols.
  • Commands: This library contains the command definitions used by the UniversalRouter.
  • IPoolManager: This interface is needed for interacting with Uniswap v4 pools. While we don't directly use it in our simple example, it's often necessary for more complex interactions with v4 pools.
  • IPermit2: This interface allows us to interact with the Permit2 contract, which provides enhanced token approval functionality.
  • StateLibrary: This provides optimized functions for interacting with the PoolManager’s state. By using StateLibrary, we can more efficiently read and manipulate pool states, which is crucial for many operations in Uniswap v4.

Step 2: Implement Token Approval with Permit2​

UniversalRouter integrates with Permit2, to enable users to have more safety, flexibility, and control over their ERC20 token approvals.

Before we can execute swaps, we need to ensure our contract can transfer tokens. We’ll implement a function to approve the Universal Router to spend tokens on behalf of our contract.

Here, for testing purposes, we set up our contract to use Permit2 with the UniversalRouter:

function approveTokenWithPermit2(
address token,
uint160 amount,
uint48 expiration
) external {
IERC20(token).approve(address(permit2), type(uint256).max);
permit2.approve(token, address(router), amount, expiration);
}

This function first approves Permit2 to spend the token, then uses Permit2 to approve the UniversalRouter with a specific amount and expiration time.

Step 3: Implementing a Swap Function​

3.1: Function Signature​

First, let’s define our function signature:

function swapExactInputSingle(
PoolKey calldata key, // PoolKey struct that identifies the v4 pool
uint128 amountIn, // Exact amount of tokens to swap
uint128 minAmountOut // Minimum amount of output tokens expected
) external returns (uint256 amountOut) {
// Implementation will follow
}

Important note:

When swapping tokens involving native ETH, we use Currency.wrap(address(0)) to represent ETH in the PoolKey struct.

struct PoolKey {
/// @notice The lower currency of the pool, sorted numerically.
/// For native ETH, Currency currency0 = Currency.wrap(address(0));
Currency currency0;
/// @notice The higher currency of the pool, sorted numerically
Currency currency1;
/// @notice The pool LP fee, capped at 1_000_000. If the highest bit is 1, the pool has a dynamic fee and must be exactly equal to 0x800000
uint24 fee;
/// @notice Ticks that involve positions must be a multiple of tick spacing
int24 tickSpacing;
/// @notice The hooks of the pool
IHooks hooks;
}

3.2: Encoding the Swap Command​

When encoding a swap command for the Universal Router, we need to choose between two types of swaps:

  1. Exact Input Swaps:

Use this swap-type when you know the exact amount of tokens you want to swap in, and you're willing to accept any amount of output tokens above your minimum. This is common when you want to sell a specific amount of tokens.

  1. Exact Output Swaps:

Use this swap-type when you need a specific amount of output tokens, and you're willing to spend up to a maximum amount of input tokens. This is useful when you need to acquire a precise amount of tokens, for example, to repay a loan or meet a specific requirement.

Next, we encode the swap command:

bytes memory commands = abi.encodePacked(uint8(Commands.V4_SWAP));

Here, we're using V4_SWAP, which tells the Universal Router that we want to perform a swap on a Uniswap v4 pool. The specific type of swap (exact input or exact output) will be determined by the V4Router actions we encode later. As we saw earlier, we encode this as a single byte, which is how the Universal Router expects to receive commands.

Check the complete list of commands.

3.3: Action Encoding​

Now, let’s encode the actions for the swap:

// Encode V4Router actions
bytes memory actions = abi.encodePacked(
uint8(Actions.SWAP_EXACT_IN_SINGLE),
uint8(Actions.SETTLE_ALL),
uint8(Actions.TAKE_ALL)
);

These actions define the sequence of operations that will be performed in our v4 swap:

  1. SWAP_EXACT_IN_SINGLE: This action specifies that we want to perform an exact input swap using a single pool.
  2. SETTLE_ALL: This action ensures all input tokens involved in the swap are properly paid. This is part of v4's settlement pattern for handling token transfers.
  3. TAKE_ALL: This final action collects all output tokens after the swap is complete.

The sequence of these actions is important as they define the complete flow of our swap operation from start to finish.

3.4: Preparing the Swap Inputs​

For our v4 swap, we need to prepare three parameters that correspond to our encoded actions:

bytes[] memory params = new bytes[](3);

// First parameter: swap configuration
params[0] = abi.encode(
IV4Router.ExactInputSingleParams({
poolKey: key,
zeroForOne: true, // true if we're swapping token0 for token1
amountIn: amountIn, // amount of tokens we're swapping
amountOutMinimum: minAmountOut, // minimum amount we expect to receive
sqrtPriceLimitX96: uint160(0), // no price limit set
hookData: bytes("") // no hook data needed
})
);

// Second parameter: specify input tokens for the swap
// encode SETTLE_ALL parameters
params[1] = abi.encode(key.currency0, amountIn);

// Third parameter: specify output tokens from the swap
params[2] = abi.encode(key.currency1, minAmountOut);

Each encoded parameter serves a specific purpose:

  1. The first parameter configures how the swap should be executed, defining the pool, amounts, and other swap-specific details
  2. The second parameter defines what tokens we're putting into the swap
  3. The third parameter defines what tokens we expect to receive from the swap

These parameters work in conjunction with the actions we encoded earlier (SWAP_EXACT_IN_SINGLE, SETTLE_ALL, and TAKE_ALL) to execute our swap operation.

3.5: Executing the Swap​

Now we can execute the swap using the Universal Router:

// Combine actions and params into inputs
inputs[0] = abi.encode(actions, params);

// Execute the swap
router.execute(commands, inputs, block.timestamp);

This prepares and executes the swap based on our encoded commands, actions, and parameters. The block.timestamp deadline parameter ensures the transaction will be executed in the current block.

3.6: (Optional) Verifying the Swap Output​

After the swap, we need to verify that we received at least the minimum amount of tokens we specified:

amountOut = IERC20(key.currency1).balanceOf(address(this));
require(amountOut >= minAmountOut, "Insufficient output amount");

3.7: Returning the Result​

Finally, we return the amount of tokens we received:

return amountOut;

This allows the caller of the function to know exactly how many tokens were received in the swap.

Here's the complete swap function that combines all the steps we've covered:

function swapExactInputSingle(
PoolKey calldata key,
uint128 amountIn,
uint128 minAmountOut
) external returns (uint256 amountOut) {
// Encode the Universal Router command
bytes memory commands = abi.encodePacked(uint8(Commands.V4_SWAP));
bytes[] memory inputs = new bytes[](1);

// Encode V4Router actions
bytes memory actions = abi.encodePacked(
uint8(Actions.SWAP_EXACT_IN_SINGLE),
uint8(Actions.SETTLE_ALL),
uint8(Actions.TAKE_ALL)
);

// Prepare parameters for each action
bytes[] memory params = new bytes[](3);
params[0] = abi.encode(
IV4Router.ExactInputSingleParams({
poolKey: key,
zeroForOne: true,
amountIn: amountIn,
amountOutMinimum: minAmountOut,
sqrtPriceLimitX96: uint160(0),
hookData: bytes("")
})
);
params[1] = abi.encode(key.currency0, amountIn);
params[2] = abi.encode(key.currency1, minAmountOut);

// Combine actions and params into inputs
inputs[0] = abi.encode(actions, params);

// Execute the swap
router.execute(commands, inputs, block.timestamp);

// Verify and return the output amount
amountOut = IERC20(key.currency1).balanceOf(address(this));
require(amountOut >= minAmountOut, "Insufficient output amount");
return amountOut;
}
Helpful?