The Balancer V2 codebase is contained in a monorepo on Github which holds several discrete packages. The packages are organized under the pkg directory, and the README contains links to several of the most important ones.
In this lesson we will review a Balancer pool contract and its associated Vault, then load the contracts on a local fork to experiment with a direct token swap performed without a front end.
I viewed the list of pools on the Balancer app to select an appropriate pool. One of the mainnet pools is an 80-20 weighted WETH/BAL pool. If you’re unfamiliar with the concept of pool weightings, please review my Balancer Introduction:
This pool has high volume and liquidity and appears to be quite active, so it’s a good choice for exploration.
The pool address is 0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56. Inspecting the Etherscan page reveals that the pool is built on the WeightedPool2Tokens.sol contract. Curiously, the Balance repo does not contain a contract by this name. Inspecting the deployment transaction for the pool, we see that it was created by a factory at address 0xA5bf2ddF098bb0Ef6d120C98217dD6B141c74EE0. The contract name for that factory is WeightedPool2TokensFactory.sol, which is also not available in the monorepo!
Searching on Github, I found a fork of the Balancer repo as of 3 years ago. It contains a version of WeightedPool2TokensFactory.sol which is very similar to the verified source for that factory contract, but does not match exactly.
Since it’s clear that the development of V2 in the monorepo was active after this deployment, I’ll prioritize the verified source on Etherscan instead. However don’t forget that the monorepo contains a test suite that I can use to inform our unit tests. I used the Uniswap V3 tests to verify the behavior of many libraries in degenbot, so this is a familiar approach.
The source code of WeightedPool2Tokens.sol is contained within the factory, so we will use it from here. I recommend opening the Etherscan verified WeightedPool2Tokens.sol contract in another tab and referring to it as we move through the contract review here.
WeightedPool2Tokens
Contract Definition
The pool contract inherits from several others. Follow the links to their source if interested:
It uses several libraries for manipulating Solidity types:
FixedPoint for uint256
WeightedPoolUserDataHelpers for bytes
WeightedPool2TokensMiscData for bytes32
The contract defines several private and internal values:
uint256 private constant _MINIMUM_BPT = 1e6
uint256 private constant _MIN_SWAP_FEE_PERCENTAGE = 1e12 // 0.0001%
uint256 private constant _MAX_SWAP_FEE_PERCENTAGE = 1e17 // 10%
bytes32 internal _miscData
uint256 private _lastInvariant
IVault private immutable _vault
bytes32 private immutable _poolId
IERC20 internal immutable _token0
IERC20 internal immutable _token1
uint256 private immutable _normalizedWeight0
uint256 private immutable _normalizedWeight1
uint256 private immutable _maxWeightTokenIndex
uint256 internal immutable _scalingFactor0
uint256 internal immutable _scalingFactor1
Events:
event OracleEnabledChanged(bool enabled)
event SwapFeePercentageChanged(uint256 swapFeePercentage);
A struct of pool parameters:
struct NewPoolParams {
IVault vault;
string name;
string symbol;
IERC20 token0;
IERC20 token1;
uint256 normalizedWeight0;
uint256 normalizedWeight1;
uint256 swapFeePercentage;
uint256 pauseWindowDuration;
uint256 bufferPeriodDuration;
bool oracleEnabled;
address owner;
}
And an important modifier that is applied to some functions:
modifier onlyVault(bytes32 poolId) {
_require(msg.sender == address(getVault()), Errors.CALLER_NOT_VAULT);
_require(poolId == getPoolId(), Errors.INVALID_POOL_ID);
_;
}