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: