An additional complication in these scenarios is the requirement to perform operations in different contracts via callbacks and hooks, which makes the usage of common memory variables impossible (due to switching the execution frame), while using storage to track intermediate balance changes is too expensive. However, the
EIP-1153 in Dencun upgrade of Ethereum introduces a new type of memory - transient storage. The new
TLOAD/TSTORE opcodes are much cheaper than regular
SLOAD/SSTORE, storing the data only during the current transaction. This type of memory is extremely useful, especially for DEXes, because all DEXes require reentrancy protection, operate with token allowances and create deterministic pool addresses with complicated constructor parameters. All these cases are well suited for
TLOAD/TSTORE.
Such operations are used in Balancer's
transient modifier to
unlock() the Vault. The
unlock() function "wraps" any operation in the Router (which can be found in
Router.sol,
BatchRouter.sol and other Routers), ensuring that no matter what happens in Router, the final check of "all token deltas are zero" will always be performed. This function operates similarly to a transient reentrancy lock, but it uses transient storage to hold per-token
deltaIsZero flags.
An example of using
unlock() to unlock the Vault and return control to the Router is here, in
swapExactIn(), in
BatchRouter.sol. Another important usage of "unlocking" the Vault is that the external protocols deny querying the state of the "unlocked" Vault, making it impossible to manipulate liquidity and prices in reentrancy scenarios. As we recall, the callback-based scheme for token transfers in/out of the protocol introduces a "natural" reentrancy vector. Additionally, Balancer's custom pools can employ external hooks, rate providers, and pool implementations that may reenter the Vault. Such behavior has been exploited in numerous DeFi attacks involving read-only reentrancy and oracle price manipulation.
The next important core functions in Balancer V3 are the
settle() and
sendTo() functions. They handle the transfers of tokens in/out of the protocol, calculating the balances differences, updating protocol reserves and performing
supplyCredit/
takeDebt actions, modifying token deltas in the current operations pack.
The
settle() function contains an interesting parameter:
amountHint, which is used to protect against "donation" attacks, when tokens are sent directly to the Vault, changing the Vault balance to manipulate the credit amount. This is mitigated by the usage of
this condition that uses
amountHint tokens for credit, no more. Any extra tokens will be simply added to the Vault's balance.
The Vault itself is an
ERC20MultiToken, holding balances/allowances of LP tokens of the pools. All token functions accept the
address pool as a first parameter, so the Vault tracks the user's balance in each pool in the form of an ERC20 token. Additionally, each pool also is an ERC20 token, as described in
BalancePoolToken.sol, but all functions of this pool token are proxied back to Vault's
ERC20MultiToken. This enables pools to have fully ERC20-compliant tokens while the Vault maintains full control over the management of balances of multiple pool tokens.
The Vault contains a substantial amount of code that exceeds the maximum contract size allowed on the Ethereum mainnet. As a result, Balancer V3 is composed of three main contracts: