Swapping with Custom Paths with the Router
This guide illustrates the process of executing swaps through the router once swap paths have been established. The examples provided encompass both single and multi-path swap types, focusing on exactIn swaps. For additional information on exactOut swaps, please refer to the Router API documentation.
To use the Balancer Smart Order Router to find efficient swap paths for a pair see this guide.
This guide is for Swapping on Balancer v3. The sdk supports swapping on v3 and Balancer v2.
Core Concepts
The core concepts of executing Swaps are the same for any programming language or framework:
- The sender must approve the Vault (not the Router) for each swap input token
- Token amount inputs/outputs are always in the raw token scale, e.g.
1 USDCshould be sent as1000000because it has 6 decimals - Transactions are always sent to the Router
- There are two different swap kinds:
- ExactIn: Where the user provides an exact input token amount.
- ExactOut: Where the user provides an exact output token amount.
- There are two subsets of a swap:
- Single Swap: A swap, tokenIn > tokenOut, using a single pool. This is the most gas efficient option for a swap of this kind.
- Multi-path Swaps: Swaps involving multiple paths but all executed in the same transaction. Each path can have its own (or the same) tokenIn/tokenOut.
The following sections provide specific implementation details for Javascript (with and without the SDK) and Solidity.
Custom Paths With The SDK
The SDK Swap object provides functionality to easily fetch updated swap quotes and create swap transactions with user defined slippage protection.
import {
ChainId,
Slippage,
SwapKind,
Swap,
SwapBuildOutputExactIn,
ExactInQueryOutput
} from "@balancer/sdk";
import { Address } from "viem";
// User defined
const swapInput = {
chainId: ChainId.SEPOLIA,
swapKind: SwapKind.GivenIn,
paths: [
{
pools: ["0x1e5b830439fce7aa6b430ca31a9d4dd775294378" as Address],
tokens: [
{
address: "0xb19382073c7a0addbb56ac6af1808fa49e377b75" as Address,
decimals: 18,
}, // tokenIn
{
address: "0xf04378a3ff97b3f979a46f91f9b2d5a1d2394773" as Address,
decimals: 18,
}, // tokenOut
],
vaultVersion: 3 as const,
inputAmountRaw: 1000000000000000000n,
outputAmountRaw: 990000000000000000n,
},
],
};
// Swap object provides useful helpers for re-querying, building call, etc
const swap = new Swap(swapInput);
console.log(
`Input token: ${swap.inputAmount.token.address}, Amount: ${swap.inputAmount.amount}`
);
console.log(
`Output token: ${swap.outputAmount.token.address}, Amount: ${swap.outputAmount.amount}`
);
// Get up to date swap result by querying onchain
const updatedOutputAmount = await swap.query(RPC_URL) as ExactInQueryOutput;
console.log(`Updated amount: ${updatedOutputAmount.expectedAmountOut}`);
// Build call data using user defined slippage
const callData = swap.buildCall({
slippage: Slippage.fromPercentage("0.1"), // 0.1%,
deadline: 999999999999999999n, // Deadline for the swap, in this case infinite
queryOutput: updatedOutputAmount,
wethIsEth: false
}) as SwapBuildOutputExactIn;
console.log(
`Min Amount Out: ${callData.minAmountOut.amount}\n\nTx Data:\nTo: ${callData.to}\nCallData: ${callData.callData}\nValue: ${callData.value}`
);
Install the Balancer SDK
The Balancer SDK is a Typescript/Javascript library for interfacing with the Balancer protocol and can be installed with:
The two main helper classes we use from the SDK are:
Swap- to build swap queries and transactionsSlippage- to simplify creating limits with user defined slippage
Providing Custom Paths To The Balancer SDK
swapInput must be of the following type:
type SwapInput = {
chainId: number;
paths: Path[];
swapKind: SwapKind;
};
chainId- the chain the swap is valid forswapKind- either aGivenInorGivenOutpaths- An array of paths that define a swap from a tokenIn>tokenOut where a path looks like:
type Path = {
pools: Address[] | Hex[];
tokens: TokenApi[];
outputAmountRaw: bigint;
inputAmountRaw: bigint;
vaultVersion: 2 | 3;
};
pools- an array of pools that will be swapped against, ordered sequentially for the path.tokens- an array of tokens that will be swapped to/from, ordered sequentially for the path.tokens[0]is the initialtokenInandtokens[length-1]is the finaltokenOutfor the path.inputAmountRaw/outputAmountRaw- the final input/output amounts for the path.vaultVersion- the version of the Balancer protocol. Note each path must use the same vaultVersion.
Using the input given above as an illustrative example:
const swapInput = {
chainId: ChainId.SEPOLIA,
swapKind: SwapKind.GivenIn,
paths: [
{
pools: ["0x1e5b830439fce7aa6b430ca31a9d4dd775294378" as Address],
tokens: [
{
address: "0xb19382073c7a0addbb56ac6af1808fa49e377b75" as Address,
decimals: 18,
}, // tokenIn
{
address: "0xf04378a3ff97b3f979a46f91f9b2d5a1d2394773" as Address,
decimals: 18,
}, // tokenOut
],
vaultVersion: 3 as const,
inputAmountRaw: 1000000000000000000n,
outputAmountRaw: 990000000000000000n,
},
],
};
We can infer:
- The swap is of the GivenIn type and is valid for Balancer v3 on Sepolia
- There is one path swapping:
- token:
0xb19382073c7a0addbb56ac6af1808fa49e377b75to0xf04378a3ff97b3f979a46f91f9b2d5a1d2394773 - using pool:
0x1e5b830439fce7aa6b430ca31a9d4dd775294378 - with an input amount of
1000000000000000000(or 1 scaled to human format) - with an output amount of
990000000000000000(or 9.9 scaled to human format)
- token:
Queries and safely setting slippage limits
Router queries allow for simulation of operations without execution. In this example, when the query function is called:
const updatedOutputAmount = await swap.query(RPC_URL) as ExactInQueryOutput;
An onchain call is used to find an updated result for the swap paths, in this case the amount of token out that would be received, updatedOutputAmount, given the original inputAmountRaw as the input.
In the next step buildCall uses the updatedOutputAmount and the user defined slippage to calculate the minAmountOut:
const callData = swap.buildCall({
slippage: Slippage.fromPercentage("1"), // 1%,
deadline: 999999999999999999n, // Deadline for the swap, in this case infinite
queryOutput: updatedOutputAmount,
wethIsEth: false
}) as SwapBuildOutputExactIn;
In the full example above, we defined our slippage as Slippage.fromPercentage('1'), meaning that we if we do not receive at least 99% of our expected updatedOutputAmount, the transaction should revert. Internally, the SDK subtracts 1% from the query output, as shown in Slippage.applyTo below:
/**
* Applies slippage to an amount in a given direction
*
* @param amount amount to apply slippage to
* @param direction +1 adds the slippage to the amount, and -1 will remove the slippage from the amount
* @returns
*/
public applyTo(amount: bigint, direction: 1 | -1 = 1): bigint {
return MathSol.mulDownFixed(
amount,
BigInt(direction) * this.amount + WAD,
);
}
Constructing the call
The output of the buildCall function provides all that is needed to submit the Swap transaction:
to- the address of the RoutercallData- the encoded call datavalue- the native asset value to be sent
It also returns the minAmountOut amount which can be useful to display/validation purposes before the transaction is sent.
Custom Paths Without The SDK
The following section illustrates swap operations on the Router through examples implemented in Javascript and Solidity.
Single Swap
The following code examples demonstrate how to execute a single token swap specifying an exact input token amount. To achieve this, we use two Router functions:
swapSingleTokenExactIn- Execute a swap specifying an exact input token amount.querySwapSingleTokenExactIn- The router query used to simulate a swap. It returns the exact amount of token out that would be received.
The Router interface for swapSingleTokenExactIn is:
/**
* @notice Executes a swap operation specifying an exact input token amount.
* @param pool Address of the liquidity pool
* @param tokenIn Token to be swapped from
* @param tokenOut Token to be swapped to
* @param exactAmountIn Exact amounts of input tokens to send
* @param minAmountOut Minimum amount of tokens to be received
* @param deadline Deadline for the swap
* @param userData Additional (optional) data required for the swap
* @param wethIsEth If true, incoming ETH will be wrapped to WETH; otherwise the Vault will pull WETH tokens
* @return amountOut Calculated amount of output tokens to be received in exchange for the given input tokens
*/
function swapSingleTokenExactIn(
address pool,
IERC20 tokenIn,
IERC20 tokenOut,
uint256 exactAmountIn,
uint256 minAmountOut,
uint256 deadline,
bool wethIsEth,
bytes calldata userData
) external payable returns (uint256 amountOut);
exactAmountIndefines the exact amount of tokenIn to send.minAmountOutdefines the minimum amount of tokenOut to receive. If the amount is less than this (e.g. because of slippage) the transaction will revert- If
wethIsEthis set totrue, the Router will deposit theexactAmountInofETHinto theWETHcontract. So, the transaction must be sent with the appropriatevalueamount deadlinethe UNIX timestamp at which the swap must be completed by - if the transaction is confirmed after this time then the transaction will fail.userDataallows additional parameters to be provided for custom pool types. In most cases it is not required and a value of0xcan be provided.
Javascript Without SDK
Resources:
Solidity
Queries should not be used onchain to set minAmountOut due to possible manipulation via frontrunning.
pragma solidity ^0.8.4;
// TODO - Assume there will be interface type package? Needs updated when released.
import "@balancer-labs/...../IRouter.sol";
contract SingleSwap {
IRouter public router;
constructor(IRouter _router) {
router = _router;
}
function singleSwap(
address pool,
IERC20 tokenIn,
IERC20 tokenOut,
uint256 exactAmountIn,
uint256 minAmountOut,
uint256 deadline,
bool wethIsEth,
bytes calldata userData
) external {
router.swapSingleTokenExactIn(
pool,
tokenIn,
tokenOut,
exactAmountIn,
minAmountOut,
deadline,
wethIsEth,
userData
);
}
}
Multi Path Swap
Note
Multi-path Swaps use the Balancer BatchRouter
The following code examples demonstrate how to execute a multi path swap specifying exact input token amounts. To achieve this, we use two Router functions:
swapExactIn- Execute a swap involving multiple paths, specifying exact input token amounts.querySwapExactIn- The router query used to simulate a swap. It returns the exact amount of token out for each swap path.
The Router interface for swapExactIn is:
/**
* @notice Executes a swap operation involving multiple paths (steps), specifying exact input token amounts.
* @param paths Swap paths from token in to token out, specifying exact amounts in.
* @param deadline Deadline for the swap
* @param wethIsEth If true, incoming ETH will be wrapped to WETH; otherwise the Vault will pull WETH tokens
* @param userData Additional (optional) data required for the swap
* @return pathAmountsOut Calculated amounts of output tokens corresponding to the last step of each given path
* @return tokensOut Calculated output token addresses
* @return amountsOut Calculated amounts of output tokens, ordered by output token address
*/
function swapExactIn(
SwapPathExactAmountIn[] memory paths,
uint256 deadline,
bool wethIsEth,
bytes calldata userData
) external payable returns (uint256[] memory pathAmountsOut, address[] memory tokensOut, uint256[] memory amountsOut);
deadlinethe UNIX timestamp at which the swap must be completed by - if the transaction is confirmed after this time then the transaction will fail.- If
wethIsEthis set totrue, the Router will deposit theexactAmountInofETHinto theWETHcontract. So, the transaction must be sent with the appropriatevalueamount userDataallows additional parameters to be provided for custom pool types. In most cases it is not required and a value of0xcan be provided.pathsan array of swap paths, in this caseSwapPathExactAmountIn, that have a number of steps,SwapPathStep, to swap a given tokenIn to tokenOut:
struct SwapPathStep {
address pool;
IERC20 tokenOut;
// If true, the "pool" is an ERC4626 Buffer. Used to wrap/unwrap tokens if pool doesn't have enough liquidity.
bool isBuffer;
}
struct SwapPathExactAmountIn {
IERC20 tokenIn;
// for each step:
// if tokenIn == pool use removeLiquidity SINGLE_TOKEN_EXACT_IN
// if tokenOut == pool use addLiquidity UNBALANCED
SwapPathStep[] steps;
uint256 exactAmountIn;
uint256 minAmountOut;
}
- each
pathdefines aminAmountOut. If the amount oftokenOutis less than this (e.g. because of slippage) the transaction will revert - pool add/remove operations can be included in the path by using a pool address as tokenIn/Out
- tokenIn == pool: router will remove liquidity from pool to a single token,
tokenOut - tokenOut == pool: router will add liquidity using
tokenIn - isBuffer: if true, this means the "pool" address is actually an ERC4626 wrapped token, and we want to use the associated buffer
- tokenIn == pool: router will remove liquidity from pool to a single token,
Javascript
Resources:
Solidity
Queries should not be used onchain to set minAmountOut due to possible manipulation via frontrunning.
pragma solidity ^0.8.4;
// TODO - Assume there will be interface type package? Needs updated when released.
import "@balancer-labs/...../IRouter.sol";
contract MultiPathSwap {
IRouter public router;
constructor(IRouter _router) {
router = _router;
}
function multiPathSwap(
SwapPathExactAmountIn[] memory paths,
uint256 deadline,
bool wethIsEth,
bytes calldata userData
) external {
router.swapExactIn(
paths,
deadline,
wethIsEth,
userData
);
}
}