How to Create a Weighted Pool using Balancer v2 Lite
- Bare bones starter kit implementation of the Balancer protocol
- Work directly with contract code (eg, no NPM dependancies)
- Walkthrough tutorial on Balancer v2 Lite
1. Introduction
Unlike working with the Uniswap codebase, getting started with the Balancer v2 monorepo can be a daunting task. The best consolidated educational resource on Balancer v2 (that I’ve found) is Balancer’s developer docs repos. After thoroughly reading through this resource, it largely orients developers to using Balancer’s NPM JavaScript libraries. After scouring multiple GitHub repos, this also seems to be how most projects are consuming Balancer as well. I’m presuming that this is most likely due to the large size of the project’s codebase.
Here, we present an alternative called Balancer v2 Lite, which serves as a bare bones starter kit for core devs who which to integrate Balancer v2 into their projects at the contract level. This repos can also serve as an educational codebase for those who want remove the obfuscation, and peel back the many layers of JavaScript to help streamline their understanding of Balancer v2 core.
2. Test Setup for Balancer v2
Here we walkthrough the basic steps that are needed to setting up a weighted pool using Balancer v2 Lite, which include:
- Deploying a vault
- Deploying test tokens
- Setting up a protocol fee provider
- Deploying a pool contract
- Funding the pool
- Performing a swap
Deploy Vault
The Vault
contract is considered to be the most innovative part of the Balancer protocol, and is heavily integrated with the BasePool
contract. This is where the protocol holds all the pool tokens and performs all the necessary bookkeeping, which serves as the the main interface for pool interactions like joins, exits and swaps.
However, prior to launching we need to setup the authorization mechanism which is responsible for protecting the protocol, which is called the TimelockAuthorizer
. This mechanism performs both a Timelock
and Authorization
in one contract; for an in-depth explanation, see this article. The typescript is as follows:
let entrypoint: MockAuthorizerAdaptorEntrypoint;
let vault: Vault;
let authorizer: Contract;
let vaultSigner: SignerWithAddress;
entrypoint = await new MockAuthorizerAdaptorEntrypoint__factory(deployer).deploy()
authorizer = await new TimelockAuthorizer__factory(deployer).deploy(admin.address, ZERO_ADDRESS, entrypoint.address, MONTH)
vault = await new Vault__factory(deployer).deploy(authorizer.address, ZERO_ADDRESS, 0, 0)
await impersonateAccount(vault.address);
await setBalance(vault.address, fp(100));
vaultSigner = await SignerWithAddress.create(ethers.provider.getSigner(vault.address));
Deploy Tokens
To engage with the Vault
, we need to next mint some test tokens. For this we engage with the TestToken
contract where its compiled code is called from the pvt directory. Before tokens are sent to the Vault
, they must be approved for appropriate allowances so that the tokens can be moved. Infinite approvals (ie, 2e^256 - 1
) can be sent to satisfy the amounts we wish to move. The typescript is as follows:
let allTokens: TokenList;
let tokens: TokenList;
const tokenAmounts = fp(100);
allTokens = await TokenList.create(TOKEN_SYMBOLS, { sorted: true });
await allTokens.mint({ to: lp, amount: tokenAmounts });
await allTokens.approve({ to: vault.address, from: lp, amount: tokenAmounts });
tokens = allTokens.subset(numTokens);
Protocol Fee Provider
Before we deploy the pool contract, we must set the protocol fee percentages. The ProtocolFeePercentagesProvider
is used to set the ground truth to determine the protocol fee percentages paid by integrators. Protocol swap fees are a percentage of the already collected swap fees. For instance, say a pool has a 1% swap fee, and there was a 10% protocol swap fee; then 0.9% of each trade would be collected for the LPs, and 0.1% would be collected for the protocol fee collector contract; see Balancer docs. The typescript setup is as follows:
let feesProvider: ProtocolFeePercentagesProvider;
feesProvider = await new ProtocolFeePercentagesProvider__factory(deployer).deploy(vault.address,
MAX_YIELD_VALUE,
MAX_AUM_VALUE);
Deploy Pool Contract
Now that we have the Vault
and ProtocolFeePercentagesProvider
deployed, we can now deploy theBasicWeightedPool
contract (with immutable weights). When pools are first launched they come equipped with an emergency pause state, hence the BASE_PAUSE_WINDOW_DURATION
and BASE_BUFFER_PERIOD_DURATION
parameters, which defined as follows:
BASE_PAUSE_WINDOW_DURATION:
allows for a contract to be paused during a max 90 days period after deploymentBASE_BUFFER_PERIOD_DURATION:
if the contract is paused when the pause window finishes, it will remain in the paused state through an additional max 30 days buffer period
Once a pool is successfully deployed, it will issue a poolId
, which is an essential component to interfacing with a pool. It can be retrieved by calling getPoolId()
from the BasicWeightedPool
contract. Other common pool data that can be retrieved are pool balances by calling getPoolTokens()
from the Vault
contract, and the pool’s swap fee by calling getSwapFeePercentage()
from the BasicWeightedPool
contract. The typescript is as follows:
let weightedPool: BasicWeightedPool;
const pool_params = {
name: NAME,
symbol: SYMBOL,
tokens: tokens.addresses,
normalizedWeights: weights,
rateProviders: [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS],
assetManagers: [ZERO_ADDRESS, ZERO_ADDRESS, ZERO_ADDRESS],
swapFeePercentage: POOL_SWAP_FEE_PERCENTAGE,
};
weightedPool = await new BasicWeightedPool__factory(deployer).deploy(pool_params,
vault.address,
feesProvider.address,
BASE_PAUSE_WINDOW_DURATION,
BASE_BUFFER_PERIOD_DURATION,
recipient.address);
Join Pool
Once your pool has successfully issued a poolId
, it is time to send it your test tokens. This is done by calling joinPool()
on the Vault
contract. Essential to calling joinPool()
is setting up a JoinPoolRequest
or ExitPoolRequest
which takes a userData
argument and specifies how the pool should be joined or exited. It is important to note that the userData
argument is a highly versatile field and must be encoded by use case; see Balancer Developer docs for more information. Once successfully transferred getPoolTokens()
can be called from the Vault
contract to check for the update. The typescript is as follows:
let vault: Vault;
const initialBalances = [fp(0.9), fp(1.8), fp(2.7)];
const joinKind = 0;
const abi = ['uint256', 'uint256[]'];
const data = [joinKind, initialBalances];
const userData = defaultAbiCoder.encode(abi,data);
const poolId = await weightedPool.getPoolId();
const request: JoinPoolRequest = {
assets: tokens.addresses,
maxAmountsIn: initialBalances,
userData: userData,
fromInternalBalance: false,
};
const tx = await vault.connect(lp).joinPool(poolId, lp.address, recipient.address, request);
SwapGivenIn / SwapGivenOut
Now that our pool has token balances, it is time to perform a test swap. Like joins, this is also done by calling swap()
on the Vault
contract. Essential to calling the swap()
function is first building our SingleSwap
and FundManagement
structs. In Balancer, there are two types of swap kinds: SwapGivenIn and SwapGivenOut. In the SingleSwap
struct we define the swap kind as a GivenIn, which indicates the exact amount of tokens that will be sent in. For more information on these structs, see Balancer Developer docs. Once the swap has been successfully implemented, getPoolTokens()
can be called from the Vault
contract to check for the update. The typescript is as follows:
let vault: Vault;
const poolId = await weightedPool.getPoolId();
const amount = fp(0.1);
const amountWithFees = fpMul(amount, POOL_SWAP_FEE_PERCENTAGE.add(fp(1)));
const singleSwap: SingleSwap = {
poolId: poolId,
kind: SwapKind.GivenIn,
assetIn: tokens.get(1).instance.address,
assetOut: tokens.get(0).instance.address,
amount: amountWithFees,
userData: '0x',
};
const funds: FundManagement = {
sender: lp.address,
recipient: recipient.address,
fromInternalBalance: false,
toInternalBalance: false,
};
const tx = await vault.connect(lp).swap(singleSwap, funds, 0, MAX_UINT256)
3. Summary
The work presented in this article is a larger body of work for doing things like: (a) simulate the behaviour of a simple liquidity pool [1]; (b) studying the effects of Impermanent Loss in an LP [2]; and © analyzing the risk profile and profitability in more advanced DeFi systems. Here, we provide the basic setup for those who are interested in getting into researching LPs. Please be sure to look out for future medium posts on this!
See GH repos for the typescript behind this presentation