Info

This page is a work in progress

ERC4626 Liquidity Buffers

Liquidity Buffers, an internal mechanism of the Vault, facilitate liquidity for pairs of an ERC4626 asset (underlying token like DAI) and the ERC4626 Vault Token (like waDAI). The Balancer Vault provides additional liquidity, enabling the entry into the ERC4626 Vault Token positions without the need to wrap or unwrap tokens, thereby avoiding higher gas costs.

ERC4626 liquidity buffers trade on a previewDeposit & previewMint basis. Meaning given an amount of 100 DAI, the liquidity buffer gives out waDAI based on the return value from previewDeposit(100 DAI).

A significant benefit of the Vault's liquidity buffers is that Liquidity Providers (LPs) can now provide liquidity in positions of 100% boosted pools (two yield-bearing assets) while simultaneously adding gas efficient batch-swaps routes.

It's important to note that ERC4626 liquidity buffers are not Balancer Pools. They are a concept internal to the Vault and only function with Tokens that comply with the ERC4626 Standard.

action: this section can use more wording work.

Info

If your organization is a DAO and you're seeking to enhance liquidity for your ERC4626 compliant token, Balancer's ERC4626 liquidity buffers can be a valuable tool. By providing POL to these buffers, you can enable LPs of your token to gain increased access to yield-bearing tokens. This arrangement allows LPs to concentrate on boosted pools, while your DAO contributes POL to the buffer.

Adding liquidity to a buffer

Liquidity can be added to a buffer for a specific token pair. This is done by invoking the addLiquidityToBuffer function, where you designate the ERC4626 Token as the buffer reference. You also specify the amounts of both the wrapped and underlying tokens that you want to add to the buffer. It's important to note that a buffer can still function with zero liquidity. It can be used to wrap and unwrap assets, meaning that even an empty buffer can facilitate swaps through the Vault.

/**
 * @notice Adds liquidity to a yield-bearing buffer (one of the Vault's internal ERC4626 token buffers).
 * @param wrappedToken Address of the wrapped token that implements IERC4626
 * @param amountUnderlyingRaw Amount of underlying tokens that will be deposited into the buffer
 * @param amountWrappedRaw Amount of wrapped tokens that will be deposited into the buffer
 * @return issuedShares the amount of tokens sharesOwner has in the buffer, denominated in underlying tokens.
 * (This is the BPT of an internal ERC4626 token buffer.)
*/
function addLiquidityToBuffer(
    IERC4626 wrappedToken,
    uint256 amountUnderlyingRaw,
    uint256 amountWrappedRaw
) external returns (uint256 issuedShares);

Removing liquidity from a buffer

After you've added liquidity to a buffer, you have the option to remove a specified amount based on the share amount. This is done by invoking the function removeLiquidityFromBuffer. This function will subsequently burn a specified amount of your bufferShares and return the corresponding amount of tokens that you had previously provided.

/**
 * @notice Removes liquidity from a yield-bearing buffer (one of the Vault's internal ERC4626 token buffers).
 * @dev Only proportional withdrawals are supported, and removing liquidity is permissioned.
 * @param wrappedToken Address of a wrapped token that implements IERC4626
 * @param sharesToRemove Amount of shares to remove from the buffer. Cannot be greater than sharesOwner's
 * total shares
 * @return removedUnderlyingBalanceRaw Amount of underlying tokens returned to the user
 * @return removedWrappedBalanceRaw Amount of wrapped tokens returned to the user
*/
function removeLiquidityFromBuffer(
    IERC4626 wrappedToken,
    uint256 sharesToRemove
) external returns (uint256 removedUnderlyingBalanceRaw, uint256 removedWrappedBalanceRaw);

Using a buffer to swap.

The swapper has the responsibility to decide whether a specific swap route should use Buffers by indicating if a given pool is a buffer. Remember: You can always use a buffer even it is does not have liquidity (instead it will simply wrap or unwrap). This is done by setting the boolean entry in the SwapPathStep struct.

The pool param in this particular case is the wrapped Tokens entrypoint. Meaning the address where a user would call deposit in. In the case of Aave it would the waUSDC.

struct SwapPathStep {
    address pool;
    IERC20 tokenOut;
    // If true, pool is an ERC4626 buffer. Used to wrap/unwrap tokens if pool doesn't have enough liquidity.
    bool isBuffer;
}

The availability of sufficient liquidity in the buffer affects the gas cost of the swap. If the buffer lacks enough liquidity, the gas cost increases. This is because the Vault has to get the additional liquidity from the lending protocol, which involves either depositing into or withdrawing from it.

Buffers aim to streamline the majority of trades by eliminating the need to wrap or unwrap the swapper's tokens. Instead, they route these tokens through the Balancer trade paths.

In the case of trading DAI to USDC via (DAI-waDAI Buffer, waDAI - waUSDC Boosted pool, USDC-waUSDC Buffer) for a 3 hop trade the SwapPathExactAmountIn would look like:

struct SwapPathExactAmountIn {
        IERC20 tokenIn;
        // For each step:
        // If tokenIn == pool, use removeLiquidity SINGLE_TOKEN_EXACT_IN.
        // If tokenOut == pool, use addLiquidity UNBALANCED.
        SwapPathStep[] steps;
        uint256 exactAmountIn;
        uint256 minAmountOut;
    }
SwapPathExactAmountIn({
    tokenIn: address(DAI),
    steps: [
        SwapPathStep({
            pool: address(waDAI), // the address where the Vault calls `deposit` or `mint` depending on SWAP_TYPE and Buffer liquidity
            tokenOut: IERC20(address(waDAI)),
            isBuffer: true
        }),
        SwapPathStep({
            pool: address(boostedPool)
            tokenOut: IERC20(address(waUSDC)),
            isBuffer: false
        }),
        SwapPathStep({
            pool: address(waUSDC)
            tokenOut: IERC20(address(USDC)),
            isBuffer: true
        })
    ],
    exactAmountIn: uint256(myExactAmountIn) // your defined amount
    minAmountOut: uint256(myMinAmountOut) // your calculated min amount out
})

The trade will execute regardless if the Buffer has enough liquidity or not. Remember: If the buffer does not have enough liquidity it will simply additionally wrap or unwrap (and incur additional gas cost).

Swapping DAI to USDC via 3 hops.

Let's consider a swap from 10k DAI to USDC. the exchangeRate of 1waDAI - DAI is 1.1 & exchangeRate for waUSDC - USDC is 1.1. Involved pools & Buffers are:

  • DAI - waDAI Buffer
  • waDAI - waUSDC Boosted Pool (100% boosted)
  • USDC - waUSDC Buffer

Considering these three pools only, the way to swap through them is via a swapExactIn operation on the BatchRouter.

  1. Swap DAI to waDAI via the DAI - waDAI Buffer
  2. Swap waDAI to waUSDC via the waDAI - waUSDC 100% Boosted pool
  3. swap waUSDC to USDC via the USDC - waUSDC Buffer

Balances with enough buffer liquidity available

Balances of pool & buffers before the batchswap:

DAIBufferBalance before SwapDAIBufferBalance after SwapwaDAIBufferBalance before SwapwaDAIBufferBalance after Swap
110k DAI120k DAI (+10k DAI)91k waDAI81909.1 waDAI (-9090.9 waDAI)
BoostedPool waDAI Balance before SwapBoostedPool waDAI Balance after SwapBoostedPool waUSDC Balance before SwapBoostedPool waUSDC Balance after Swap
900k waDAI909090.9 waDAI (+9090.9 waDAI)900k waUSDC890909.1 waUSDC (-9090.9 waUSDC)
USDCBufferBalance before SwapUSDCBufferBalance after SwapwaUSDCBufferBalance before SwapwaUSDCBufferBalance after Swap
100k USDC90000 USDC (-10k USDC)91k waUSDC100090.9 waUSDC (+9090.9 waUSDC)

Balances without enough buffer liquidity available in DAI - waDAI buffer

Consider now an EXACT_IN trade of 60k DAI to USDC. The DAI - waDAI buffer does not have enough liquidity to support the trade from its reserves, so it calls into the waDAI contract to wrap DAI to waDAI (amount) and additionally rebalances the buffer to balanced reserves.

DAIBufferBalance before SwapDAIBufferBalance after SwapwaDAIBufferBalance before SwapwaDAIBufferBalance after Swap
50k DAI44761 DAI (-5239 DAI)40k waDAI40692 waDAI (+692 waDAI)
BoostedPool waDAI Balance before SwapBoostedPool waDAI Balance after SwapBoostedPool waUSDC Balance before SwapBoostedPool waUSDC Balance after Swap
900k waDAI954545.45 waDAI (+54545.45 waDAI)900k waUSDC845454.55 waUSDC (-54545.45 waUSDC)
USDCBufferBalance before SwapUSDCBufferBalance after SwapwaUSDCBufferBalance before SwapwaUSDCBufferBalance after Swap
100k USDC40000 USDC (-60000 USDC)91k waUSDC145545.45 waUSDC (+54545.45 waUSDC)

Even though the DAI - waDAI buffer did not have enough liquidity the trade was successfully routed via Balancer. The difference now is that the Vault utilized the buffer internal wrapping capability to wrap DAI into waDAI & rebalanced itself.