Degen Code

Degen Code

Share this post

Degen Code
Degen Code
Balancer — V2 Vault Contract
Copy link
Facebook
Email
Notes
More

Balancer — V2 Vault Contract

Part III: How A Balancer Swap Actually Works

Dec 16, 2024
∙ Paid
1

Share this post

Degen Code
Degen Code
Balancer — V2 Vault Contract
Copy link
Facebook
Email
Notes
More
1
Share

Part II of the Balancer series focused on the V2 Pool contract.

Balancer — V2 Pool Contract

Balancer — V2 Pool Contract

BowTiedDevil
·
December 11, 2024
Read full story

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.

This post is for paid subscribers

Already a paid subscriber? Sign in
© 2025 BowTiedDevil
Privacy ∙ Terms ∙ Collection notice
Start writingGet the app
Substack is the home for great culture

Share

Copy link
Facebook
Email
Notes
More