Continuing our exploration of the Balancer ecosystem, let’s turn our attention to the batch swap functionality of the Vault contract.
Part II covered the Weighted Balancer pool variant, and Part III covered its interaction with the Vault.
This lesson will review the Vault’s batch swap feature, which allows the Vault to perform multiple swaps in a single transaction. Uniswap V3 offers a similar feature via the Multicall interface to the V3 Router and Universal Router contracts.
Batch Swaps
The batch swap function appears in Swaps.sol, which the Vault contract inherits:
function batchSwap(
    SwapKind kind,
    BatchSwapStep[] memory swaps,
    IAsset[] memory assets,
    FundManagement memory funds,
    int256[] memory limits,
    uint256 deadline
)
    external
    payable
    override
    nonReentrant
    whenNotPaused
    authenticateFor(funds.sender)
    returns (int256[] memory assetDeltas)
{
    // 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);
    InputHelpers.ensureInputLengthMatch(assets.length, limits.length);
    // Perform the swaps, updating the Pool token balances and computing the net Vault asset deltas.
    assetDeltas = _swapWithPools(swaps, assets, funds, kind);
    // Process asset deltas, by either transferring assets from the sender (for positive deltas) or to the recipient
    // (for negative deltas).
    uint256 wrappedEth = 0;
    for (uint256 i = 0; i < assets.length; ++i) {
        IAsset asset = assets[i];
        int256 delta = assetDeltas[i];
        _require(delta <= limits[i], Errors.SWAP_LIMIT);
        if (delta > 0) {
            uint256 toReceive = uint256(delta);
            _receiveAsset(asset, toReceive, funds.sender, funds.fromInternalBalance);
            if (_isETH(asset)) {
                wrappedEth = wrappedEth.add(toReceive);
            }
        } else if (delta < 0) {
            uint256 toSend = uint256(-delta);
            _sendAsset(asset, toSend, funds.recipient, funds.toInternalBalance);
        }
    }
    // Handle any used and remaining ETH.
    _handleRemainingEth(wrappedEth);
}It is a simple function, focused mainly on input validation and token amount bookkeeping.
Let’s consider the function inputs:
SwapKind kind
BatchSwapStep[] memory swaps
IAsset[] memory assets
FundManagement memory funds
int256[] memory limits
uint256 deadline
We studied the SwapKind and FundManagement structs in Part III. IAsset, limit, and deadline are simple types covered in Part II.
So the only new type we need to inspect is BatchSwapStep, which is defined in IVault.sol:
struct BatchSwapStep {
    bytes32 poolId;
    uint256 assetInIndex;
    uint256 assetOutIndex;
    uint256 amount;
    bytes userData;
}The poolId, amount, and userData elements are straightforward and familiar. However assetInIndex and assetOutIndex are new.
The comments above batchSwap() in IVault.sol clarify that the entries in swaps specify the input and output tokens by their index in the assets array. So we should be aware that multi-pool swaps need to perform some pre-transaction token indexing and provide the appropriate index at each pool that matches the order we’ve specified.
Swaps At Each Pool
From the perspective of the swap through each pool, it operates similarly to the single swap function.
To keep the function small, the logic for performing the chained swaps is offloaded to a _swapWithPools() function:



