Developers
Search…
Batch Swaps

Why should I use a batchSwap?

Tokens that aren't in the same pool
Let's say we want to trade TokenA for TokenC, but we only have pools with [TokenA, TokenB] and [TokenB, TokenC]. We can swap A -> B and B -> C.
Routes with better prices than Single Swaps
Let's say we still want to trade TokenA for TokenC, but now there's a [TokenA, TokenC] pool. We could use a Single Swap there, but there might be a better price routing by swapping A -> B and B -> C.
For additional information, check out the Batch Swaps page in Resources.

Code Walkthrough

Select your desired programming language in the tabs below for the relevant tutorial.
js
Python
This script assumes that the address you're using has already granted token spend allowances to the Vault. If you do not do that first, your transaction will fail.
Let's step through a JavaScript Batch Swap example chunk by chunk.

Dependencies

1
const Web3 = require("web3");
2
const fs = require("fs");
3
const BigNumber = require("bignumber.js");
4
const Tx = require('ethereumjs-tx').Transaction;
5
const open = require('open');
Copied!
This sample relies on web3.js to interact with on-chain Smart Contracts and a few other libraries. These dependencies can be found in the package.json file in the JavaScript sample repository.

Connecting to RPC and setting up your account

1
// Load private key and connect to RPC endpoint
2
const rpc_endpoint = process.env.RPC_ENDPOINT;
3
const private_key = process.env.KEY_PRIVATE;
4
if (rpc_endpoint == undefined || private_key == undefined || private_key == "") {
5
throw new Error("You must set environment variables for RPC_ENDPOINT and KEY_PRIVATE");
6
}
7
const web3 = new Web3(new Web3.providers.HttpProvider(rpc_endpoint));
8
const account = web3.eth.accounts.privateKeyToAccount(private_key);
9
const address = account.address;
10
11
// Define network settings
12
const network = "kovan";
13
const block_explorer_url = "https://kovan.etherscan.io/";
14
const chain_id = "42";
15
const gas_price = "2";
Copied!
This section connects to the Ethereum (or other EVM compatible) blockchain using the RPC_ENDPOINT environment variable provided either in the shell, or in the bash script helper. Similarly, it initialized an Ethereum account based on the private key provided with KEY_PRIVATE.
We then define some network-specific settings. In this example, we're using the Kovan Testnet. The block_explorer_url makes it easy to see that status of our transaction in a web browser. Each chain has a different chain_id, so make sure this is set properly for the network you're using. We also manually set a gas_price (denominated in gwei).

Initializing the Balancer Vault

1
// Load contract for Balancer Vault
2
const address_vault = "0xBA12222222228d8Ba445958a75a0704d566BF2C8";
3
const path_abi_vault = "../../abis/Vault.json";
4
let abi_vault = JSON.parse(fs.readFileSync(path_abi_vault));
5
const contract_vault = new web3.eth.Contract(abi_vault, address_vault);
Copied!
The sample repository has a copy of the Balancer V2 Vault ABI that you'll need to interact with the contract itself. On every network that Balancer V2 is officially deployed, the Vault address is the same, and is easily recognizable starting with "0xBA1222222222".
This chunk loads the Vault from its on-chain contract address, and its ABI makes it easy to make calls to it directly.

Swap Settings

1
// Where are the tokens coming from/going to?
2
const fund_settings = {
3
"sender": address,
4
"recipient": address,
5
"fromInternalBalance": false,
6
"toInternalBalance": false
7
};
8
9
// When should the transaction timeout?
10
const deadline = BigNumber(999999999999999999);
Copied!
Here, we're specifying that the sender/recipient for the tokens going into/out of the trade are both the account that we initialized the script with. Note that with this granularity, it is possible to make a swap that sends the tokens to a different address.
We specify that {to/from}InternalBalance are both False. This will be the default use case for most users; however, you may have a use case that would benefit from Internal Balances.
The deadline for a transaction is the time (in Unix timestamp) after which it will no longer attempt to make a trade. If a trade expires, it will still take some gas to process the failed transaction, but it will be cheaper than a transaction failing for a different reason.

Defining our pools and tokens

1
// Pool IDs
2
const pool_WETH_USDC = "0x3a19030ed746bd1c3f2b0f996ff9479af04c5f0a000200000000000000000004";
3
const pool_BAL_WETH = "0x61d5dc44849c9c87b0856a2a311536205c96c7fd000200000000000000000000";
4
5
// Token addresses (checksum format)
6
const token_BAL = "0x41286Bb1D3E870f3F750eB7E1C25d7E48c8A1Ac7".toLowerCase();
7
const token_USDC = "0xc2569dd7d0fd715B054fBf16E75B001E5c0C1115".toLowerCase();
8
const token_WETH = "0xdFCeA9088c8A88A76FF74892C1457C17dfeef9C1".toLowerCase();
9
10
// Token data
11
const token_data = {};
12
token_data[token_BAL] =
13
{
14
"symbol": "BAL",
15
"decimals": "18",
16
"limit": "0"
17
};
18
token_data[token_USDC] =
19
{
20
"symbol":"USDC",
21
"decimals":"6",
22
"limit":"100"
23
};
24
token_data[token_WETH] =
25
{
26
"symbol": "WETH",
27
"decimals": "18",
28
"limit": "0"
29
};
Copied!
Here, we list our Pool IDs, token contract addresses, and relevant token data. When entering token contract addresses, we enforce that they must be in lowercase to avoid issues with sorting later.
It is important to get your decimals set correctly for each token, otherwise you may send far more or far fewer tokens than you intended.
To protect users from front-running or the market changing rapidly, they supply a list of limits for each token involved in the swap, where either the maximum number of tokens to send (by passing a positive value) or the minimum amount of tokens to receive (by passing a negative value) is specified. See that in this example, we are willing to send at most 100 USDC, and receive as few as 0 BAL, WETH. Setting your receive limits to 0 is generally a very bad idea (it means we are willing to accept 100% slippage on our trade), but this is an example on the Kovan testnet, so tokens are valueless here.

Swap Steps

1
const swap_steps = [
2
{
3
"poolId": pool_WETH_USDC,
4
"assetIn": token_USDC,
5
"assetOut": token_WETH,
6
"amount": 100
7
},
8
{
9
"poolId": pool_BAL_WETH,
10
"assetIn": token_WETH,
11
"assetOut": token_BAL,
12
"amount": 0
13
}
14
];
15
16
// SwapKind is an Enum. This example handles a GIVEN_IN swap.
17
// https://github.com/balancer-labs/balancer-v2-monorepo/blob/0328ed575c1b36fb0ad61ab8ce848083543070b9/pkg/vault/contracts/interfaces/IVault.sol#L497
18
// 0 = GIVEN_IN, 1 = GIVEN_OUT
19
const swap_kind = 0;
Copied!
Next, we define our swap_steps. Each step in this list is its own swap with a pool. The first step here is clearly a swap of 100 USDC for WETH in pool_WETH_USDC (amounts will be scaled for decimals later). The second step is less obvious. You may notice that the amount in the second swap is 0. Here, the 0 value is used to say "take the output of the previous swap and use it as my input." The reason for this is that the expected output of the trade could change slightly between the times that the trade is requested and when it is actually executed.
We also must specify that this swap is created with a known amount GIVEN_IN; we are giving the pool 100 USDC for an estimated output. It is also possible to create a trade with a fixed amount GIVEN_OUT.

Token ordering

1
var token_addresses = Object.keys(token_data);
2
token_addresses.sort();
3
const token_indices = {};
4
for (var i = 0; i < token_addresses.length; i++) {
5
token_indices[token_addresses[i]] = i;
6
}
Copied!
Token ordering is very important in the Balancer Vault; each pool stores its tokens sorted numerically. Because of this, we will need to sort our own token lists when interacting with pools. When calling the contract itself, we must refer to the tokens by their index in this sorted list. The token_indicies loop creates a dictionary that gives us each token's index in a sorted list to make the bookkeeping easier.

Building our structs

1
const swap_steps_struct = [];
2
for (const step of swap_steps) {
3
const swap_step_struct = {
4
poolId: step["poolId"],
5
assetInIndex: token_indices[step["assetIn"]],
6
assetOutIndex: token_indices[step["assetOut"]],
7
amount: BigNumber(step["amount"] * Math.pow(10, token_data[step["assetIn"]]["decimals"])).toString(),
8
userData: '0x'
9
};
10
swap_steps_struct.push(swap_step_struct);
11
}
12
13
const fund_struct = {
14
sender: web3.utils.toChecksumAddress(fund_settings["sender"]),
15
fromInternalBalance: fund_settings["fromInternalBalance"],
16
recipient: web3.utils.toChecksumAddress(fund_settings["recipient"]),
17
toInternalBalance: fund_settings["toInternalBalance"]
18
};
Copied!
When we call the Vault contract, we need to pack our data into structs, specifically the ones here referred to as swap_steps_struct and fund_structs.
swap_steps_struct are of type BatchSwapStep, which is defined here:
1
struct BatchSwapStep {
2
bytes32 poolId;
3
uint256 assetInIndex;
4
uint256 assetOutIndex;
5
uint256 amount;
6
bytes userData;
7
}
Copied!
The values that may cause confusion
  • amount: Either the amount of tokens we are sending to the pool or want to receive from the pool, depending on if the SwapKind is GIVEN_IN or GIVEN_OUT. As shown in the code, make sure that the amount is scaled according to the number of decimals for your token.
  • userData: Any additional data which the pool requires to perform the swap. This allows pools to have more flexible swapping logic in future - for all current Balancer pools this can be left empty, here entered as '0x'.
fund_structs are simpler FundManagements structs as defined here:
1
struct FundManagement {
2
address sender;
3
bool fromInternalBalance;
4
address payable recipient;
5
bool toInternalBalance;
6
}
Copied!
The only real "gotcha" here is to make sure your addresses are in checksum format.

Just a couple more formattings

1
const token_limits = [];
2
const checksum_tokens = [];
3
for (const token of token_addresses) {
4
token_limits.push(BigNumber((token_data[token]["limit"]) * Math.pow(10, token_data[token]["decimals"])).toString());
5
checksum_tokens.push(web3.utils.toChecksumAddress(token));
6
}
Copied!
Here, we're scaling our token_limits to account for the token-specific decimals, and converting all our token addresses to checksum format instead of lowercase.

Building the function

1
const batch_swap_function = contract_vault.methods.batchSwap(
2
swap_kind,
3
swap_steps_struct,
4
checksum_tokens,
5
fund_struct,
6
token_limits,
7
deadline.toString()
8
);
Copied!
Here, we're packing our properly formatted structs and other values into the batchSwap function.

Setting the remaining relevant parameters in an async method

1
async function buildAndSend() {
2
var gas_estimate;
3
try {
4
gas_estimate = await batch_swap_function.estimateGas();
5
}
6
catch(err) {
7
gas_estimate = 200000;
8
console.log("Failed to estimate gas, attempting to send with", gas_estimate, "gas limit...");
9
}
10
11
const tx_object = {
12
'chainId': chain_id,
13
'gas': web3.utils.toHex(gas_estimate),
14
'gasPrice': web3.utils.toHex(web3.utils.toWei(gas_price,'gwei')),
15
'nonce': await web3.eth.getTransactionCount(address),
16
'data': batch_swap_function.encodeABI(),
17
'to': address_vault
18
};
Copied!
Here, we attempt to estimate a gas price. In the event it fails, 200k gas is a safe estimate for the gas limit on a two-swap batchSwap. The remaining lines set the chainId, gas, gasPrice, nonce, data, and to address.

Sending and viewing the transaction

1
const tx = new Tx(tx_object);
2
const signed_tx = await web3.eth.accounts.signTransaction(tx_object, private_key)
3
.then(signed_tx => web3.eth.sendSignedTransaction(signed_tx['rawTransaction']));
4
console.log("Sending transaction...");
5
const tx_hash = signed_tx["logs"][0]["transactionHash"];
6
const url = block_explorer_url + "tx/" + tx_hash;
7
open(url);
8
}
9
buildAndSend();
Copied!
Finally, we sign the transaction with our private_key, and broadcast the transaction to be added to the blockchain. As a convenience, the last two lines of the buildAndSend() method create a link to Etherscan and opens a tab in the user's default browser.
This script assumes that the address you're using has already granted token spend allowances to the Vault. If you do not do that first, your transaction will fail.
Let's step through a Python Batch Swap example chunk by chunk.

Dependencies

1
from web3 import Web3
2
import eth_abi
3
4
import os
5
import json
6
from decimal import *
7
import webbrowser
Copied!
This sample relies on web3.py to interact with on-chain Smart Contracts and a few other libraries. These dependencies can be found in the requirements.txt file in the Python sample repository.

Connecting to RPC and setting up your account

1
# Load private key and connect to RPC endpoint
2
rpc_endpoint = os.environ.get("RPC_ENDPOINT")
3
private_key = os.environ.get("KEY_PRIVATE")
4
if rpc_endpoint is None or private_key is None or private_key == "":
5
print("\n[ERROR] You must set environment variables for RPC_ENDPOINT and KEY_PRIVATE\n")
6
quit()
7
web3 = Web3(Web3.HTTPProvider(rpc_endpoint))
8
account = web3.eth.account.privateKeyToAccount(private_key)
9
address = account.address
10
11
# Define network settings
12
network = "kovan"
13
block_explorer_url = "https://kovan.etherscan.io/"
14
chain_id = 42
15
gas_price = 2
Copied!
This section connects to the Ethereum (or other EVM compatible) blockchain using the RPC_ENDPOINT environment variable provided either in the shell, or in the bash script helper. Similarly, it initialized an Ethereum account based on the private key provided with KEY_PRIVATE.
We then define some network-specific settings. In this example, we're using the Kovan Testnet. The block_explorer_url makes it easy to see that status of our transaction in a web browser. Each chain has a different chain_id, so make sure this is set properly for the network you're using. We also manually set a gas_price (denominated in gwei).

Initializing the Balancer Vault

1
# Load contract for Balancer Vault
2
address_vault = "0xBA12222222228d8Ba445958a75a0704d566BF2C8"
3
path_abi_vault = '../abis/Vault.json'
4
with open(path_abi_vault) as f:
5
abi_vault = json.load(f)
6
contract_vault = web3.eth.contract(
7
address=web3.toChecksumAddress(address_vault),
8
abi=abi_vault
9
)
Copied!
The sample repository has a copy of the Balancer V2 Vault ABI that you'll need to interact with the contract itself. On every network that Balancer V2 is officially deployed, the Vault address is the same, and is easily recognizable starting with "0xBA1222222222".
This chunk loads the Vault from its on-chain contract address, and its ABI makes it easy to make calls to it directly.

Swap Settings

1
# Where are the tokens coming from/going to?
2
fund_settings = {
3
"sender": address,
4
"recipient": address,
5
"fromInternalBalance": False,
6
"toInternalBalance": False
7
}
8
9
# When should the transaction timeout?
10
deadline = 999999999999999999
Copied!
Here, we're specifying that the sender/recipient for the tokens going into/out of the trade are both the account that we initialized the script with. Note that with this granularity, it is possible to make a swap that sends the tokens to a different address.
We specify that {to/from}InternalBalance are both False. This will be the default use case for most users; however, you may have a use case that would benefit from Internal Balances.
The deadline for a transaction is the time (in Unix timestamp) after which it will no longer attempt to make a trade. If a trade expires, it will still take some gas to process the failed transaction, but it will be cheaper than a transaction failing for a different reason.

Defining our pools and tokens

1
# Pool IDs
2
pool_WETH_USDC = "0x3a19030ed746bd1c3f2b0f996ff9479af04c5f0a000200000000000000000004"
3
pool_BAL_WETH = "0x61d5dc44849c9c87b0856a2a311536205c96c7fd000200000000000000000000"
4
5
# Token addresses
6
token_BAL = "0x41286Bb1D3E870f3F750eB7E1C25d7E48c8A1Ac7".lower()
7
token_USDC = "0xc2569dd7d0fd715B054fBf16E75B001E5c0C1115".lower()
8
token_WETH = "0xdFCeA9088c8A88A76FF74892C1457C17dfeef9C1".lower()
9
10
# Token data
11
token_data = {
12
token_BAL:{
13
"symbol":"BAL",
14
"decimals":"18",
15
"limit":"0"
16
},
17
token_USDC:{
18
"symbol":"USDC",
19
"decimals":"6",
20
"limit":"100"
21
},
22
token_WETH:{
23
"symbol":"WETH",
24
"decimals":"18",
25
"limit":"0"
26
}
27
}
Copied!
Here, we list our Pool IDs, token contract addresses, and relevant token data. When entering token contract addresses, we enforce that they must be in lowercase to avoid issues with sorting later.
It is important to get your decimals set correctly for each token, otherwise you may send far more or far fewer tokens than you intended.
To protect users from front-running or the market changing rapidly, they supply a list of limits for each token involved in the swap, where either the maximum number of tokens to send (by passing a positive value) or the minimum amount of tokens to receive (by passing a negative value) is specified. See that in this example, we are willing to send at most 100 USDC, and receive as few as 0 BAL, WETH. Setting your receive limits to 0 is generally a very bad idea (it means we are willing to accept 100% slippage on our trade), but this is an example on the Kovan testnet, so tokens are valueless here.

Swap Steps

1
swap_steps = [
2
{
3
"poolId":pool_WETH_USDC,
4
"assetIn":token_USDC,
5
"assetOut":token_WETH,
6
"amount": "100"
7
},
8
{
9
"poolId":pool_BAL_WETH,
10
"assetIn":token_WETH,
11
"assetOut":token_BAL,
12
"amount":"0"
13
}
14
]
15
16
# SwapKind is an Enum. This example handles a GIVEN_IN swap.
17
# https://github.com/balancer-labs/balancer-v2-monorepo/blob/0328ed575c1b36fb0ad61ab8ce848083543070b9/pkg/vault/contracts/interfaces/IVault.sol#L497
18
swap_kind = 0 #0 = GIVEN_IN, 1 = GIVEN_OUT
Copied!
Next, we define our swap_steps. Each step in this list is its own swap with a pool. The first step here is clearly a swap of 100 USDC for WETH in pool_WETH_USDC (amounts will be scaled for decimals later). The second step is less obvious. You may notice that the amount in the second swap is 0. Here, the 0 value is used to say "take the output of the previous swap and use it as my input." The reason for this is that the expected output of the trade could change slightly between the times that the trade is requested and when it is actually executed.
We also must specify that this swap is created with a known amount GIVEN_IN; we are giving the pool 100 USDC for an estimated output. It is also possible to create a trade with a fixed amount GIVEN_OUT.

Token ordering

1
token_addresses = list(token_data.keys())
2
token_addresses.sort()
3
token_indices = {token_addresses[idx]:idx for idx in range(len(token_addresses))}
Copied!
Token ordering is very important in the Balancer Vault; each pool stores its tokens sorted numerically. Because of this, we will need to sort our own token lists when interacting with pools. When calling the contract itself, we must refer to the tokens by their index in this sorted list. The token_indicies line creates a dictionary that gives us each token's index in a sorted list to make the bookkeeping easier.

Building our structs

1
user_data_encoded = eth_abi.encode_abi(['uint256'], [0])
2
swaps_step_structs = []
3
for step in swap_steps:
4
swaps_step_struct = (
5
step["poolId"],
6
token_indices[step["assetIn"]],
7
token_indices[step["assetOut"]],
8
int(Decimal(step["amount"]) * 10 ** Decimal((token_data[step["assetIn"]]["decimals"]))),
9
user_data_encoded
10
)
11
swaps_step_structs.append(swaps_step_struct)
12
13
fund_struct = (
14
web3.toChecksumAddress(fund_settings["sender"]),
15
fund_settings["fromInternalBalance"],
16
web3.toChecksumAddress(fund_settings["recipient"]),
17
fund_settings["toInternalBalance"]
18
)
Copied!
When we call the Vault contract, we need to pack our data into structs, specifically the ones here referred to as swap_step_structs and fund_structs.
swap_step_structs are of type BatchSwapStep, which is defined here:
1
struct BatchSwapStep {
2
bytes32 poolId;
3
uint256 assetInIndex;
4
uint256 assetOutIndex;
5
uint256 amount;
6
bytes userData;
7
}
Copied!
The values that may cause confusion
  • amount: Either the amount of tokens we are sending to the pool or want to receive from the pool, depending on if the SwapKind is GIVEN_IN or GIVEN_OUT. As shown in the code, make sure that the amount is scaled according to the number of decimals for your token.
  • userData: Any additional data which the pool requires to perform the swap. This allows pools to have more flexible swapping logic in future - for all current Balancer pools this can be left empty, as shown for user_data_encoded.
fund_structs are simpler FundManagements structs as defined here:
1
struct FundManagement {
2
address sender;
3
bool fromInternalBalance;
4
address payable recipient;
5
bool toInternalBalance;
6
}
Copied!
The only real "gotcha" here is to make sure your addresses are in checksum format.

Just a couple more formattings

1
token_limits = [int(Decimal(token_data[token]["limit"]) * 10 ** Decimal(token_data[token]["decimals"])) for token in token_addresses]
2
checksum_tokens = [web3.toChecksumAddress(token) for token in token_addresses]
Copied!
Here, we're scaling our token_limits to account for the token-specific decimals, and converting all our token addresses to checksum format instead of lowercase.

Building the function

1
batch_swap_function = contract_vault.functions.batchSwap(
2
swap_kind,
3
swaps_step_structs,
4
checksum_tokens,
5
fund_struct,
6
token_limits,
7
deadline
8
)
Copied!
Here, we're packing our properly formatted structs and other values into the batchSwap function.

Setting the remaining relevant parameters

1
try:
2
gas_estimate = batch_swap_function.estimateGas()
3
except:
4
gas_estimate = 200000
5
print("Failed to estimate gas, attempting to send with", gas_estimate, "gas limit...")
6
7
data = batch_swap_function.buildTransaction(
8
{
9
'chainId': chain_id,
10
'gas': gas_estimate,
11
'gasPrice': web3.toWei(gas_price, 'gwei'),
12
'nonce': web3.eth.get_transaction_count(address),
13
}
14
)
Copied!
Here, we attempt to estimate a gas price. In the event it fails, 200k gas is a safe estimate for the gas limit on a two-swap batchSwap. The remaining lines set the chain_id, gas_estimate, gas_price, and nonce.

Sending and viewing the transaction

1
signed_tx = web3.eth.account.sign_transaction(data, private_key)
2
tx_hash = web3.eth.send_raw_transaction(signed_tx.rawTransaction).hex()
3
print("Sending transaction...")
4
url = block_explorer_url + "tx/" + tx_hash
5
webbrowser.open_new_tab(url)
Copied!
Finally, we sign the transaction with our private_key, and broadcast the transaction to be added to the blockchain. As a convenience, the last two lines create a link to Etherscan and opens a tab in the user's default browser.