Developers
Search…
Single Swaps

Why should I use a single swap?

You'll want to use Single Swaps when you're making a trade between two tokens in one pool. While it's possible to do this with a one-step Batch Swap, using a Single Swap will save ~6,000 gas.
For additional information, check out the Single Swap 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 Single 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_BAL_WETH = "0x61d5dc44849c9c87b0856a2a311536205c96c7fd000200000000000000000000";
3
4
// Token addresses (checksum format)
5
const token_BAL = "0x41286Bb1D3E870f3F750eB7E1C25d7E48c8A1Ac7".toLowerCase();
6
const token_WETH = "0xdFCeA9088c8A88A76FF74892C1457C17dfeef9C1".toLowerCase();
7
8
// Token data
9
const token_data = {};
10
token_data[token_BAL] =
11
{
12
"symbol": "BAL",
13
"decimals": "18",
14
"limit": "0"
15
};
16
token_data[token_WETH] =
17
{
18
"symbol": "WETH",
19
"decimals": "18",
20
"limit": "1"
21
};
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.
When setting your token limits, you must know that there are two types of swaps: GIVEN_IN and GIVEN_OUT. In a GIVEN_IN swap, the amount you provide is the number of your input token you're giving the pool, while for a GIVEN_OUT swap, you provide the number of tokens that you expect to receive. The limits are used to control the maximum/minimum amounts for the opposite side of the trade that you are not setting exact numbers for.
Swap Type
amount
TokenIn Limit
TokenOut Limit
GIVEN_IN
Exact amount of TokenIn you will send
Same as TokenIn amount
Minimum amount of TokenOut you'll accept
GIVEN_OUT
Exact amount of TokenOut you will receive
Maximum amount of TokenIn you're willing to give
Same as TokenOut amount
In the above example code, we set our token_BAL limit to 0, which means we are willing to accept 100% slippage on our trade. That is generally a very bad idea, but this is an example on the Kovan testnet, so tokens are valueless here.

Swap definition

1
const swap = {
2
"poolId": pool_BAL_WETH,
3
"assetIn": token_WETH,
4
"assetOut": token_BAL,
5
"amount": 1
6
};
7
8
// SwapKind is an Enum. This example handles a GIVEN_IN swap.
9
// https://github.com/balancer-labs/balancer-v2-monorepo/blob/0328ed575c1b36fb0ad61ab8ce848083543070b9/pkg/vault/contracts/interfaces/IVault.sol#L497
10
// 0 = GIVEN_IN, 1 = GIVEN_OUT
11
const swap_kind = 0;
Copied!
Next, we define our swap. We must specify that this swap is created with a known amount GIVEN_IN; we are giving the pool 1 WETH for an estimated output of BAL. It is also possible to create a trade with a fixed amount GIVEN_OUT.

Building our structs

1
const swap_struct = {
2
poolId: swap["poolId"],
3
kind: swap_kind,
4
assetIn: web3.utils.toChecksumAddress(swap["assetIn"]),
5
assetOut: web3.utils.toChecksumAddress(swap["assetOut"]),
6
amount: BigNumber(swap["amount"] * Math.pow(10, token_data[swap["assetIn"]]["decimals"])).toString(),
7
userData: '0x'
8
};
9
10
const fund_struct = {
11
sender: web3.utils.toChecksumAddress(fund_settings["sender"]),
12
fromInternalBalance: fund_settings["fromInternalBalance"],
13
recipient: web3.utils.toChecksumAddress(fund_settings["recipient"]),
14
toInternalBalance: fund_settings["toInternalBalance"]
15
};
Copied!
When we call the Vault contract, we need to pack our data into structs, specifically the ones here referred to as swap_struct and fund_struct.
swap_structs are of type SingleSwap, which is defined here:
1
struct SingleSwap {
2
bytes32 poolId;
3
SwapKind kind;
4
IAsset assetIn;
5
IAsset assetOut;
6
uint256 amount;
7
bytes userData;
8
}
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 one more formatting

1
const token_limit = BigNumber((token_data[swap["assetIn"]]["limit"]) * Math.pow(10, token_data[swap["assetIn"]]["decimals"])).toString();
Copied!
Here, we're scaling our token_limit to account for the token-specific decimals.

Building the function

1
const single_swap_function = contract_vault.methods.swap(
2
swap_struct,
3
fund_struct,
4
token_limit,
5
deadline.toString()
6
);
Copied!
Here, we're packing our properly formatted structs and other values into the swap function.

Setting the remaining relevant parameters in an async function

1
async function buildAndSend() {
2
var gas_estimate;
3
try {
4
gas_estimate = await single_swap_function.estimateGas();
5
}
6
catch(err) {
7
gas_estimate = 100000;
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': single_swap_function.encodeABI(),
17
'to': address_vault
18
};
Copied!
Here, we attempt to estimate a gas price. In the event it fails, 100k gas is a safe estimate for the gas limit on a swap. 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 Single 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_BAL_WETH = "0x61d5dc44849c9c87b0856a2a311536205c96c7fd000200000000000000000000"
3
4
# Token addresses
5
token_BAL = "0x41286Bb1D3E870f3F750eB7E1C25d7E48c8A1Ac7".lower()
6
token_WETH = "0xdFCeA9088c8A88A76FF74892C1457C17dfeef9C1".lower()
7
8
# Token data
9
token_data = {
10
token_BAL:{
11
"symbol":"BAL",
12
"decimals":"18",
13
"limit":"0"
14
},
15
token_WETH:{
16
"symbol":"WETH",
17
"decimals":"18",
18
"limit":"1"
19
}
20
}
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.
When setting your token limits, you must know that there are two types of swaps: GIVEN_IN and GIVEN_OUT. In a GIVEN_IN swap, the amount you provide is the number of your input token you're giving the pool, while for a GIVEN_OUT swap, you provide the number of tokens that you expect to receive. The limits are used to control the maximum/minimum amounts for the opposite side of the trade that you are not setting exact numbers for.
Swap Type
amount
TokenIn Limit
TokenOut Limit
GIVEN_IN
Exact amount of TokenIn you will send
Same as TokenIn amount
Minimum amount of TokenOut you'll accept
GIVEN_OUT
Exact amount of TokenOut you will receive
Maximum amount of TokenIn you're willing to give
Same as TokenOut amount
In the above example code, we set our token_BAL limit to 0, which means we are willing to accept 100% slippage on our trade. That is generally a very bad idea, but this is an example on the Kovan testnet, so tokens are valueless here.

Swap definition

1
swap = {
2
"poolId":pool_BAL_WETH,
3
"assetIn":token_WETH,
4
"assetOut":token_BAL,
5
"amount":"1"
6
}
7
8
# SwapKind is an Enum. This example handles a GIVEN_IN swap.
9
# https://github.com/balancer-labs/balancer-v2-monorepo/blob/0328ed575c1b36fb0ad61ab8ce848083543070b9/pkg/vault/contracts/interfaces/IVault.sol#L497
10
swap_kind = 0 #0 = GIVEN_IN, 1 = GIVEN_OUT
Copied!
Next, we define our swap. We must specify that this swap is created with a known amount GIVEN_IN; we are giving the pool 1 WETH for an estimated output of BAL. It is also possible to create a trade with a fixed amount GIVEN_OUT.

Building our structs

1
user_data_encoded = eth_abi.encode_abi(['uint256'], [0])
2
3
swap_struct = (
4
swap["poolId"],
5
swap_kind,
6
web3.toChecksumAddress(swap["assetIn"]),
7
web3.toChecksumAddress(swap["assetOut"]),
8
int(Decimal(swap["amount"]) * 10 ** Decimal((token_data[swap["assetIn"]]["decimals"]))),
9
user_data_encoded
10
)
11
12
fund_struct = (
13
web3.toChecksumAddress(fund_settings["sender"]),
14
fund_settings["fromInternalBalance"],
15
web3.toChecksumAddress(fund_settings["recipient"]),
16
fund_settings["toInternalBalance"]
17
)
18
Copied!
When we call the Vault contract, we need to pack our data into structs, specifically the ones here referred to as swap_struct and fund_struct.
swap_structs are of type SingleSwap, which is defined here:
1
struct SingleSwap {
2
bytes32 poolId;
3
SwapKind kind;
4
IAsset assetIn;
5
IAsset assetOut;
6
uint256 amount;
7
bytes userData;
8
}
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 one more formatting

1
token_limit = int(Decimal(token_data[swap["assetIn"]]["limit"]) * 10 ** Decimal(token_data[swap["assetIn"]]["decimals"]))
Copied!
Here, we're scaling our token_limit to account for the token-specific decimals.

Building the function

1
single_swap_function = contract_vault.functions.swap(
2
swap_struct,
3
fund_struct,
4
token_limit,
5
deadline
6
)
Copied!
Here, we're packing our properly formatted structs and other values into the swap function.

Setting the remaining relevant parameters

1
try:
2
gas_estimate = single_swap_function.estimateGas()
3
except:
4
gas_estimate = 100000
5
print("Failed to estimate gas, attempting to send with", gas_estimate, "gas limit...")
6
7
data = single_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, 100k gas is a safe estimate for the gas limit on a swap. 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.