Skip to main content
Helpful?

Swapping on Uniswap v4

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.

Although it's technically possible to interact directly with the PoolManager contract for swaps, this approach is not recommended due to its complexity and potential inefficiencies. Instead, the Universal Router is the preferred method, as it abstracts away these complexities. By using the Universal Router, developers and users can ensure a more straightforward, efficient, and standardized approach to executing swaps on v4 pools, aligning with best practices for Uniswap interactions.

Configuring Universal Router for Uniswap v4 Swaps

Set up a foundry 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/
[...]

Step 1: Set Up the Project​

First, we need to set up our project and import the necessary dependencies. 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 corresponds to a specific action in our swap:

  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

The sequence of these parameters must match the sequence of actions we defined earlier (SWAP_EXACT_IN_SINGLE, SETTLE_ALL, and TAKE_ALL).

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?