Skip to content

Commit

Permalink
Added quickstart guide for swap (#820)
Browse files Browse the repository at this point in the history
  • Loading branch information
seeyijie authored Dec 2, 2024
1 parent 6bf4448 commit 290660c
Show file tree
Hide file tree
Showing 2 changed files with 302 additions and 21 deletions.
46 changes: 27 additions & 19 deletions docs/contracts/v4/guides/09-swap-routing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Each command is encoded as a single byte (`bytes1`) with a specific structure:
- 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
# Configuring Universal Router for Uniswap v4 Swaps

## Use Cases

Expand All @@ -48,9 +48,27 @@ Developers might need to configure the Universal Router for swapping on Uniswap

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

## Step 1: Set Up the Project
## 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.
First, we need to set up our project and install the necessary dependencies.
```bash
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.

```solidity
// SPDX-License-Identifier: MIT
Expand Down Expand Up @@ -89,17 +107,7 @@ In this step, we’re importing the necessary contracts and interfaces:
- `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.

**Important note:**

To use the Universal Router imports, you'll need to set up remappings in your project. This can be done using a `remappings.txt` file:

```
@uniswap/universal-router/=lib/universal-router/
[...]
```

## Step 2: Implement Token Approval with Permit2
## Step 2: Implement Token Approval with Permit2

`UniversalRouter` integrates with [Permit2](https://github.com/Uniswap/permit2), to enable users to have more safety, flexibility, and control over their ERC20 token approvals.

Expand All @@ -109,8 +117,8 @@ Here, for testing purposes, we set up our contract to use Permit2 with the Unive

```solidity
function approveTokenWithPermit2(
address token,
uint160 amount,
address token,
uint160 amount,
uint48 expiration
) external {
IERC20(token).approve(address(permit2), type(uint256).max);
Expand All @@ -120,7 +128,7 @@ function approveTokenWithPermit2(

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
## Step 3: Implementing a Swap Function

### 3.1: Function Signature

Expand Down Expand Up @@ -148,11 +156,11 @@ When encoding a swap command for the Universal Router, we need to choose between

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.
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.

2. 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.
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:

Expand Down
277 changes: 275 additions & 2 deletions docs/contracts/v4/quickstart/03-swap.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,279 @@
title: Swap
---

## 🚧 Under construction 🚧
# 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.

Please see https://www.v4-by-example.org/swap
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:
```bash
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.

```solidity
// 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.

**Important note:**

To use the Universal Router imports, you'll need to set up remappings in your project. This can be done using a `remappings.txt` file:

```
@uniswap/universal-router/=lib/universal-router/
[...]
```

## Step 2: Implement Token Approval with Permit2

`UniversalRouter` integrates with [Permit2](https://github.com/Uniswap/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:

```solidity
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:

```solidity
function swapExactInputSingle(
PoolKey calldata key,
uint128 amountIn,
uint128 minAmountOut
) external returns (uint256 amountOut) {
// Implementation will follow
}
```

The function takes:

- `key`: The [PoolKey](https://github.com/Uniswap/v4-core/blob/main/src/types/PoolKey.sol) struct that identifies the v4 pool
- `amountIn`: The exact amount of tokens to swap
- `minAmountOut`: The minimum amount of output tokens expected

### 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.

2. 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:

```solidity
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](https://docs.uniswap.org/contracts/universal-router/technical-reference#command).

### 3.3: Action Encoding

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

```solidity
// 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:

```solidity
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:

```solidity
// 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:

```solidity
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:

```solidity
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:

```solidity
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;
}
```

0 comments on commit 290660c

Please sign in to comment.