How to Create a Weighted Pool using Balancer v2 Lite

Ian Moore, PhD
Coinmonks
Published in
5 min readJan 18, 2024

--

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 deployment
  • BASE_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

--

--