Balancer Javascript SDK
A JavaScript SDK which provides commonly used utilties for interacting with Balancer Protocol V2.
How to run the examples (Javascript)?
In order to run the examples provided, you need to follow the next steps:
-
git clone https://github.com/balancer-labs/balancer-sdk.git
-
cd balancer-sdk
-
cd balancer-js
-
Create a .env file in the balancer-js folder
-
In the .env file you will need to define and initialize the following variables
We have defined both Alchemy and Infura, because some of the examples use Infura, others use Alchemy. However, feel free to modify accordingly and use your favourite one.
ALCHEMY_URL=[ALCHEMY HTTPS ENDPOINT]
INFURA=[Infura API KEY]
TRADER_KEY=[MetaMask PRIVATE KEY]
Some examples also require the following Tenderly config parameters to be defined:
TENDERLY_ACCESS_KEY=[TENDERLY API ACCESS KEY]
TENDERLY_PROJECT=[TENDERLY PROJECT NAME]
TENDERLY_USER=[TENDERLY USERNAME]
-
Run 'npm run node', this runs a local Hardhat Network
-
Open a new terminal
-
cd to balancer-js
-
Install ts-node using: npm install ts-node
-
Install tsconfig-paths using: npm install --save-dev tsconfig-paths
-
Generate contracts using: npm run typechain:generate
-
Run one of the provided examples (eg: npm run examples:run -- examples/join.ts)
Installation
Getting Started
import { BalancerSDK, BalancerSdkConfig, Network } from '@balancer-labs/sdk';
const config: BalancerSdkConfig = {
network: Network.MAINNET,
rpcUrl: `https://mainnet.infura.io/v3/${process.env.INFURA}`,
};
const balancer = new BalancerSDK(config);
In some examples we present a way to make end to end trades against mainnet state. To run them you will need to setup a localhost test node using tools like ganache, hardhat, anvil.
Installation instructions for:
-
Hardhat
To start a MAINNET forked node:
- Set env var:
ALCHEMY_URL=[ALCHEMY HTTPS ENDPOINT for MAINNET]
- Run:
npm run node
To start a GOERLI forked node:
- Set env var:
ALCHEMY_URL_GOERLI=[ALCHEMY HTTPS ENDPOINT for GOERLI]
- Run:
npm run node:goerli
-
Anvil - use with caution, still experimental.
To start a forked node:
anvil -f FORKABLE_RPC_URL (optional pinned block: --fork-block-number XXX)
Swaps Module
Exposes complete functionality for token swapping. An example of using the module with data fetched from the subgraph:
const route = balancer.swaps.findRouteGivenIn({
tokenIn,
tokenOut,
amount,
gasPrice,
maxPools,
});
const transactionAttributes = balancer.swaps.buildSwap({
userAddress,
swapInfo: route,
kind: 0,
deadline,
maxSlippage,
});
const { to, data, value } = transactionAttributes;
const transactionResponse = await signer.sendTransaction({ to, data, value });
SwapsService
The SwapsService provides function to query and make swaps using Balancer V2 liquidity.
const swaps = new swapService({
network: Network;
rpcUrl: string;
});
Examples
You can run each example with npm run examples:run -- examples/exampleName.ts
#queryBatchSwap
The Balancer Vault provides a method to simulate a call to batchSwap.
This function performs no checks on the sender or recipient or token balances or approvals. Note that this function is not 'view' (due to implementation details): the client code must explicitly execute eth_call instead of eth_sendTransaction.
@param batchSwap - BatchSwap information used for query.
@param batchSwap.kind - either exactIn or exactOut.
@param batchSwap.swaps - sequence of swaps.
@param batchSwap.assets - array contains the addresses of all assets involved in the swaps.
@returns Returns an array with the net Vault asset balance deltas. Positive amounts represent tokens (or ETH) sent to the Vault, and negative amounts represent tokens (or ETH) sent by the Vault. Each delta corresponds to the asset at the same index in the assets
array.
swaps.queryBatchSwap(batchSwap: {
kind: SwapType,
swaps: BatchSwapStep[],
assets: string[]
}): Promise<BigNumberish[]>
Example
#encodeBatchSwap
Static method to encode a batch swap.
NB: This method doesn't execute a batchSwap -- it returns an ABI byte string containing the data of the function call on a contract, which can then be sent to the network (ex. sendTransaction). to be executed. See example for more info.
Swaps.encodeBatchSwap(batchSwap: {
kind: SwapType,
swaps: BatchSwapStep[],
assets: string[],
funds: FundManagement,
limits: number[],
deadline: string
}): string
Example
Swap Service: Flash Swaps
A Flash Swap is a special type of batch swap where the caller doesn't need to own or provide any of the input tokens -- the caller is essentially taking a "flash loan" (an uncollateralized loan) from the Balancer Vault. The full amount of the input token must be returned to the Vault by the end of the batch (plus any swap fees), however any excess of an output tokens can be sent to any address.
IMPORTANT: A "simple" flash swap is an arbitrage executed with only two tokens and two pools,
swapping in the first pool and then back in the second pool for a profit. For more
complex flash swaps, you will have to use batch swap directly.
Gotchas:
- Both pools must have both assets (tokens) for swaps to work
- No pool token balances can be zero
- If the flash swap isn't profitable, the internal flash loan will fail.
#encodeSimpleFlashSwap
Static method to encode a simple flash swap method for a batchSwap.
NB: This method doesn't execute any swaps -- it returns an ABI byte string containing the data of the function call on a contract, which can then be sent to the network (ex. sendTransaction). to be executed. See example for more info.
Swaps.encodeSimpleFlashSwap(simpleFlashSwap: {
flashLoanAmount: string,
poolIds: string[],
assets: string[]
walletAddress: string[]
}): string
Example
#querySimpleFlashSwap
Method to test if a simple flash swap is valid and see potential profits.
swaps.querySimpleFlashSwap(batchSwap: {
kind: SwapType,
swaps: BatchSwapStep[],
assets: string[]
}): string
Example
Pricing
Spot Price functionality allowing user to query spot price for token pair.
calcSpotPrice
Find Spot Price for pair in specific pool.
const balancer = new BalancerSDK(sdkConfig);
const pool = await balancer.pools.find(poolId);
const spotPrice = await pool.calcSpotPrice(
ADDRESSES[network].DAI.address,
ADDRESSES[network].BAL.address
);
#getSpotPrice
Find Spot Price for a token pair - finds most liquid path and uses this as reference SP.
const pricing = new Pricing(sdkConfig);
@param { string } tokenIn Token in address.
@param { string } tokenOut Token out address.
@param { SubgraphPoolBase[] } pools Optional - Pool data. Will be fetched via dataProvider if not supplied.
@returns { string } Spot price.
async getSpotPrice(
tokenIn: string,
tokenOut: string,
pools: SubgraphPoolBase[] = []
): Promise<string>
Example
Simulating pool joins and exists
The Balancer Vault provides a method to simulate join or exit calls to a pool.
These function allows you to perform a dry run before sending an actual transaction, without checking the sender / recipient or token balances / approvals. Note that this function is not 'view' (due to implementation details): the client code must explicitly execute eth_call
instead of eth_sendTransaction
.
Simulating joins
There are two ways to join a pool:
joinExactIn
: Joining the pool with known token amounts. This is the most commonly used method.joinExactOut
: Asking the pool for the expected liquidity when we know how much BPT we want back.
In this documentation, we will focus on the first method (joinExactIn
) for joining a pool with known token amounts.
const pool = await sdk.pools.find(poolId);
const maxAmountsIn = pool.tokenList.map(
(t) => forEachTokenSpecifyAmountYouWantToJoinWith
);
const queryParams = pool.buildQueryJoinExactIn({ maxAmountsIn });
const response = await balancerContracts.balancerHelpers.queryJoin(
...queryParams
);
const { bptOut, amountsIn } = response;
response
will return:
bptOut
: The expected pool token amount returned by the pool.amountsIn
: The same as maxAmountsIn
Simulating exits
There are three ways to exit a pool:
exitToSingleToken
: Exiting liquidity to a single underlying token is the simplest method. However, if the amount of liquidity being exited is a significant portion of the pool's total liquidity, it may result in price slippage.exitProportionally
: Exiting liquidity proportionally to all pool tokens. This is the most commonly used method. However ComposableStable
pool type doesn't support it.exitExactOut
: Asking the pool for the expected pool token amount when we know how much token amounts we want back.
In this example, we will focus on the first method (exitProportionally
).
const pool = await sdk.pools.find(poolId);
const queryParams = pool.buildQueryJoinExactIn({ bptIn });
const response = await balancerContracts.balancerHelpers.queryJoin(
...queryParams
);
const { bptIn, amountsOut } = response;
response
will return:
amountsOut
: Token amounts returned by the pool.bptIn
: The same as intput bptIn
More examples: https://github.com/balancer-labs/balancer-sdk/blob/master/balancer-js/examples/pools/queries.ts
Joining Pools
Joining with pool tokens
Exposes Join functionality allowing user to join pools with its pool tokens.
const balancer = new BalancerSDK(sdkConfig);
const pool = await balancer.pools.find(poolId);
const { to, functionName, attributes, data } = pool.buildJoin(params);
#buildJoin
Builds join pool transaction parameters with exact tokens in and minimum BPT out based on slippage tolerance
buildJoin: (
joiner: string,
tokensIn: string[],
amountsIn: string[],
slippage: string
) => JoinPoolAttributes;
where JoinPoolAttributes
is:
export interface JoinPoolAttributes {
to: string;
functionName: string;
attributes: JoinPool;
data: string;
value?: BigNumber;
minBPTOut: string;
expectedBPTOut: string;
priceImpact: string;
}
Example
#buildInitJoin (Weighted Pool)
Builds a init join transaction for weighted pool.
buildInitJoin({
joiner,
poolId,
tokensIn,
amountsIn,
}) => InitJoinPoolAttributes
Example
Available pool types:
Joining nested pools
Exposes Join functionality allowing user to join a pool that has pool tokens that are BPTs of other pools, e.g.:
CS0
/ \
CS1 CS2
/ \ / \
DAI USDC USDT FRAX
Can join with tokens: DAI, USDC, USDT, FRAX, CS1_BPT, CS2_BPT
async generalisedJoin(
poolId: string,
tokens: string[],
amounts: string[],
userAddress: string,
slippage: string,
signer: JsonRpcSigner,
simulationType: SimulationType,
authorisation?: string
): Promise<{
to: string;
encodedCall: string;
minOut: string;
expectedOut: string;
priceImpact: string;
}>
Example
Exit Pool
Exposes Exit functionality allowing user to exit pools.
const balancer = new BalancerSDK(sdkConfig);
const pool = await balancer.pools.find(poolId);
const { to, functionName, attributes, data } = pool.buildExitExactBPTIn(params);
#buildExitExactBPTIn
Builds an exit transaction with exact BPT in and minimum token amounts out based on slippage tolerance.
buildExitExactBPTIn: (
exiter: string,
bptIn: string,
slippage: string,
shouldUnwrapNativeAsset?: boolean,
singleTokenOut?: string
) => ExitExactBPTInAttributes;
where ExitExactBPTInAttributes
is:
export interface ExitExactBPTInAttributes extends ExitPoolAttributes {
to: string;
functionName: string;
attributes: ExitPool;
data: string;
expectedAmountsOut: string[];
minAmountsOut: string[];
}
Example
Available pool types:
- Weighted Example
- Composable Stable Example
- OBS: Only ComposableStable >V2 supports proportional exits
- Meta Stable
- Stable
#buildExitExactTokensOut
Builds an exit transaction with exact tokens out and maximum BPT in based on slippage tolerance.
buildExitExactTokensOut: (
exiter: string,
tokensOut: string[],
amountsOut: string[],
slippage: string
) => ExitExactTokensOutAttributes;
where ExitExactTokensOutAttributes
is:
export interface ExitExactTokensOutAttributes extends ExitPoolAttributes {
to: string;
functionName: string;
attributes: ExitPool;
data: string;
expectedBPTIn: string;
maxBPTIn: string;
}
Example
Available pool types:
- Weighted Example
- Composable Stable
- Meta Stable
- Stable
Exiting nested pools
Exposes Exit functionality allowing user to exit a pool that has pool tokens that are BPTs of other pools, e.g.:
CS0
/ \
CS1 CS2
/ \ / \
DAI USDC USDT FRAX
Can exit with CS0_BPT proportionally to: DAI, USDC, USDT and FRAX
async generalisedExit(
poolId: string,
amount: string,
userAddress: string,
slippage: string,
signer: JsonRpcSigner,
simulationType: SimulationType,
authorisation?: string,
unwrapTokens = false
): Promise<{
to: string;
encodedCall: string;
tokensOut: string[];
expectedAmountsOut: string[];
minAmountsOut: string[];
priceImpact: string;
}>
Example
Factory
Creating Pools
WeightedPool
Builds a transaction to create a weighted pool.
create({
factoryAddress,
name,
symbol,
tokenAddresses,
weights,
swapFee,
owner,
}) => {
to?: string;
data: BytesLike;
}
Example
Composable Stable Pool
Builds a transaction to create a composable stable pool.
create({
factoryAddress,
name,
symbol,
tokenAddresses,
amplificationParameter,
rateProviders,
tokenRateCacheDurations,
exemptFromYieldProtocolFeeFlags,
swapFee,
owner,
}) => {
to?: string;
data: BytesLike;
}
Example
Linear Pool
Builds a transaction to create a linear pool.
create({
name,
symbol,
mainToken,
wrappedToken,
upperTarget,
swapFeeEvm,
owner,
protocolId,
}: LinearCreatePoolParameters) => {
to?: string;
data: BytesLike;
}
Example
RelayerService
Relayers are (user opt-in, audited) contracts that can make calls to the vault (with the transaction “sender” being any arbitrary address) and use the sender’s ERC20 vault allowance, internal balance or BPTs on their behalf.
const relayer = new relayerService(
swapsService: SwapsService;
rpcUrl: string;
);
Pools Impermanent Loss
impermanent loss (IL) describes the percentage by which a pool is worth less than what one would have if they had instead just held the tokens outside the pool
Service
Algorithm
Using the variation delta formula:
where 𝚫Pi represents the difference between the price for a single token at the date of joining the pool and the current price.
tokens = pool.tokens;
weights = tokens.map((token) => token.weight);
exitPrices = tokens.map((token) => tokenPrices.find(token.address));
entryPrices = tokens.map((token) =>
tokenPrices.findBy('timestamp', {
address: token.address,
timestamp: timestamp,
})
);
assets = tokens.map((token) => ({
priceDelta: this.getDelta(
entryPrices[token.address],
exitPrices[token.address]
),
weight: weights[i],
}));
poolValueDelta = assets.reduce(
(result, asset) =>
result * Math.pow(Math.abs(asset.priceDelta + 1), asset.weight),
1
);
holdValueDelta = assets.reduce(
(result, asset) => result + Math.abs(asset.priceDelta + 1) * asset.weight,
0
);
const IL = poolValueDelta / holdValueDelta - 1;
Usage
async impermanentLoss(
timestamp: number,
pool: Pool
): Promise<number>
const pool = await sdk.pools.find(poolId);
const joins = (await sdk.data.findByUser(userAddress)).filter(
(it) => it.type === 'Join' && it.poolId === poolId
);
const join = joins[0];
const IL = await pools.impermanentLoss(join.timestamp, pool);
Example
Claim Tokens
Service
Claim Tokens for a veBAL Holders
Pseudocode
const defaultClaimableTokens = [
'0x7B50775383d3D6f0215A8F290f2C9e2eEBBEceb2',
'0xA13a9247ea42D743238089903570127DdA72fE44',
'0xba100000625a3754423978a60c9317c58a424e3D',
]
const claimableTokens: string[] = userDefinedClaimableTokens ?? defaultClaimableTokens;
const balances = await ClaimService.getClaimableVeBalTokens(userAddress, claimableTokens) {
return await this.feeDistributor.callStatic.claimTokens(userAddress,claimableTokens);
}
const txData = await getClaimableVeBalTokens.buildClaimVeBalTokensRequest(userAddress, claimableTokens) {
data = feeDistributorContract.claimBalances(userAddress, claimableTokens);
to = feeDistributorContract.encodeFunctionData('claimTokens', [userAddress, claimableTokens])
}
signer.request(txData).then(() => { ... });
Claim Pools Incentives
Pseudocode
gauges = LiquidityGaugesRepository.fetch();
claimableTokensPerGauge = LiquidityGaugesMulticallRepository.getClaimableTokens(gauges, accountAddress) {
if (MAINNET) {
claimableTokens = this.multicall.aggregate('claimable_tokens', gauges, accountAddress);
claimableReward = gauge.rewardData.forEach(this.multicall.aggregate('claimable_reward', gauges, accountAddress, rewardToken);
return aggregate(claimableReward, claimableTokens);
} else {
return gauge.rewardData.forEach(this.multicall.aggregate('claimable_reward', gauges, accountAddress, rewardToken);
}
};
it returns encoded callable data to be fed to a signer and then to send to the gauge contract.
if (MAINNET) {
const callData = balancerMinterInterface.encodeFunctionData('mintMany', [
gaugeAddresses,
]);
return { to: balancerMinterAddress, data: callData };
} else {
const callData = gaugeClaimHelperInterface.encodeFunctionData(
'claimRewardsFromGauges',
[gaugeAddresses, userAddress]
);
return { to: gaugeClaimHelperAddress, data: callData };
}
Licensing
GNU General Public License Version 3 (GPL v3).