Part II of the Balancer series focused on the V2 Pool contract.
It would be unsatisfying to cover the pool without doing a swap, so I discussed the Vault contract enough to demonstrate how to do a simple swap before wrapping up.
This lesson will cover the Vault contract in more detail, tracing the complex contract interactions that occur between it and a Balancer pool during the swap previously demonstrated.
I will continue the demonstration using the WETH-BAL contract at 0x5c6ee304399dbdb9c8ef030ab642b10820db8f56 and the Vault contract at 0xBA12222222228d8Ba445958a75a0704d566BF2C8.
Single Swaps
The swap function we used in the last lesson is presented again here:
function swap(
SingleSwap memory singleSwap,
FundManagement memory funds,
uint256 limit,
uint256 deadline
)
external
payable
override
nonReentrant
whenNotPaused
authenticateFor(funds.sender)
returns (uint256 amountCalculated)
{
// The deadline is timestamp-based: it should not be relied upon for sub-minute accuracy.
// solhint-disable-next-line not-rely-on-time
_require(block.timestamp <= deadline, Errors.SWAP_DEADLINE);
// This revert reason is for consistency with `batchSwap`: an equivalent `swap` performed using that function
// would result in this error.
_require(singleSwap.amount > 0, Errors.UNKNOWN_AMOUNT_IN_FIRST_SWAP);
IERC20 tokenIn = _translateToIERC20(singleSwap.assetIn);
IERC20 tokenOut = _translateToIERC20(singleSwap.assetOut);
_require(tokenIn != tokenOut, Errors.CANNOT_SWAP_SAME_TOKEN);
// Initializing each struct field one-by-one uses less gas than setting all at once.
IPoolSwapStructs.SwapRequest memory poolRequest;
poolRequest.poolId = singleSwap.poolId;
poolRequest.kind = singleSwap.kind;
poolRequest.tokenIn = tokenIn;
poolRequest.tokenOut = tokenOut;
poolRequest.amount = singleSwap.amount;
poolRequest.userData = singleSwap.userData;
poolRequest.from = funds.sender;
poolRequest.to = funds.recipient;
// The lastChangeBlock field is left uninitialized.
uint256 amountIn;
uint256 amountOut;
(amountCalculated, amountIn, amountOut) = _swapWithPool(poolRequest);
_require(singleSwap.kind == SwapKind.GIVEN_IN ? amountOut >= limit : amountIn <= limit, Errors.SWAP_LIMIT);
_receiveAsset(singleSwap.assetIn, amountIn, funds.sender, funds.fromInternalBalance);
_sendAsset(singleSwap.assetOut, amountOut, funds.recipient, funds.toInternalBalance);
// If the asset in is ETH, then `amountIn` ETH was wrapped into WETH.
_handleRemainingEth(_isETH(singleSwap.assetIn) ? amountIn : 0);
}
Here we will review each code section and uncover the interaction between the Vault and the pool.
Pre-Swap Validation
The contract performs due diligence, requiring that the deadline has not passed, the requested amount is positive, and the requested tokens are not duplicated.
ERC-20 Identification
An interesting feature of the Vault is that it can accept or send Ether using a proxy address for assetIn
or assetOut
. This address is defined in AssetHelpers.sol as the zero address (0x0000000000000000000000000000000000000000):
address private constant _ETH = address(0);
The Vault’s asset transfer handler functions have special modes to handle Ether instead of standard ERC-20 tokens. They trigger when assetIn
or assetOut
is set to this address. When withdrawing Ether, WETH is unwrapped prior to transfer to the user. When when depositing Ether, it is wrapped into WETH before being deposited into the pool.
The Vault has a limited receive
function defined in AssetTransfersHandler.sol that prevents users from sending Ether to the contract by mistake. It will only accept transfers from the WETH contract, which allows the unwrapping described above to work:
receive() external payable {
_require(msg.sender == address(_WETH()), Errors.ETH_TRANSFER);
}
PoolRequest Definition
Once these checks are made, the Vault begins to craft a request which will be executed at the specified pool.