Vault and vault-like contracts’ shared vulnerabilities
Now that we’ve introduced the ERC-4626 standard and identified the shared characteristics between ERC-4626 compliant vault contracts, non ERC-4626 vault contracts, and vault-like contracts, we can discuss the vulnerabilities that are shared among these contracts. For simplicity, when I refer to smart contracts in this post, I’m referring to all three categories of vault contracts.
When a user deposits a quantity of an asset into a contract, they receive a quantity of a new asset, or share, proportional to the total deposited assets. This share may be a separate token, but in some cases it is simply an internal state variable. Therefore, the relationship between shares and underlying assets is a ratio, where the numerator is the total quantity of shares multiplied by the quantity of assets to be deposited and the denominator is the current total quantity of underlying assets.
This calculation is the source of our first vulnerability: share inflation. Share inflation occurs when an attacker manipulates the shares-to-deposited-assets ratio. There are two prerequisites that must be met in order for an attacker to manipulate this ratio:
- The contract has no, or very low, deposit balance.
- The contract’s share calculation denominator must be able to move independently of the numerator.
The first prerequisite is met by all ERC-4626, non ERC-4626, and vault-like contracts that have been created. One thing to note is that this prerequisite may not be required in all cases, but we’ll talk more about that in part three. The second prerequisite is a little trickier to understand, so let’s go over it.
As you may recall, we previously mapped the deposit calculation numerator to the total shares multiplied by the deposited asset quantity and the denominator to the total asset quantity in the contract. Taking this mapping into account, we can re-state the second prerequisite as:
The contract’s current total asset quantity must be able to move independently of the total shares multiplied by the deposited asset quantity.
This statement is intrinsically true for all ERC-4626, non ERC-4626, and vault-like contracts because it enables the mechanism that allows contracts to reward users who have deposited assets into the contract. Rewards for a contract are transferred directly to the contract itself, thus inflating the value of each share that a user holds. When users redeem shares for assets, they will receive more assets than they had deposited due to the rewards that have been deposited directly into the contract. For example, if a contract has a quantity of 100 assets and 100 shares, then each share is worth 1 asset. If 100 assets are then transferred to the contract as rewards, then each share is now worth 2 assets.
It is important to note that the normal deposit mechanism cannot be used for adding rewards to a contract. If the deposit mechanism were to be used, shares would be created and given to the depositor (most likely another smart contract within a protocol), leaving the ratio of shares to total assets deposited unchanged. This approach would not provide any incentive for users to deposit funds.
Abusing the Reward Mechanism
The way users are rewarded for depositing assets into a contract is also the mechanism that attackers can use to manipulate the initial state of a contract into over-inflating the value of their shares. By being the first depositor, an attacker is able to set the quantity of underlying asset in the contract and set the initial ratio of shares to underlying asset. Typically, an attacker will deposit 1 wei of any asset and receive 1 share. This is because the first deposit cannot use the ratio calculation because it would involve division by zero. Therefore, the first deposit calculation simply returns a number of shares equal to the quantity of assets deposited.
For readers that have implemented ERC-4626 vaults, the above code snippet may look familiar. In fact, this code snippet comes from OpenZeppelin’s own ERC-4626 contract template v4.7! Surprisingly, very few ERC-4626 contract templates implement their own mitigation to the share inflation vulnerability. This means that share inflation is very likely to be present in protocols that implement ERC-4626. However, OpenZeppelin recently released a new version of their ERC-4626 template that mitigates share inflation. We will talk more about the mitigations for share inflation in the final part of this deep-dive series.
Now that we understand the conditions required for this vulnerability to be present and have our code snippet reference, we can go over the attack scenario. Notice in the previous code snippet that the first deposit simply returns a quantity of shares in direct proportion to the quantity of assets deposited. This means if a user deposited 1 WETH, or 1e18 WETH due to decimals scaling, they would receive 1e18 shares.
Therefore, as the first depositor, an attacker will be able to set the ratio of shares to assets deposited to an arbitrarily small value by depositing 1 wei of an asset and receiving 1 share. Then, when a victim submits a transaction to deposit assets, the attacker initiates the second step of the attack. By directly transferring a quantity of asset to the contract similarly to how the contract itself would reward users, the attacker is able to manipulate the denominator of the deposit calculation independently of the numerator. If the attacker transfers enough assets such that the denominator would be greater than the numerator when a victim user goes to deposit, the attacker can cause division truncation during the calculation. Note that the attacker would need to place their direct deposit transaction immediately before the legitimate deposit transaction of the victim, so front-running would be necessary.
A brief note about division truncation: In Solidity, when you divide an integer by another integer, the expected type of the output is an integer (reference). Additionally, Solidity does not support floating or fixed point numbers, aka decimals. Therefore, when you divide an integer by another integer that would result in a decimal value, the decimal is rounded to zero. This is also known as truncation. For example, if you divide 5 by 2 in Solidity, the output will be 2 and not 2.5, as the 0.5 is truncated to 0. If you were to divide 2 by 5, the result would be 0.
Now back to the hack. By directly transferring enough assets to the contract before a victim deposits their own assets such that the denominator is greater than the numerator, the attacker is able to cause division truncation. This means that victims receive 0 shares for their deposits, and will not be able to withdraw the assets they just deposited because they have no shares that can be used to redeem their deposited assets! Then, when the attacker redeems 1 share, it is worth the full amount of assets that are in the contract, so 1 wei + the assets the attacker directly deposited + the victim’s deposited assets. The 1 share the attacker owns is now worth more than it was originally due to the attacker manipulating the contract, hence the term “share inflation.”
Let’s walk through an example scenario of how this attack would play out with real numbers:
- A new ERC-4626, non ERC-4626, or vault-like contract is deployed, where the asset it takes in is WETH. WETH has a decimals value of 18.
- Alice, the attacker, notices a new contract has been deployed. She deposits 1 wei of WETH into the contract and receives 1 share.
- Bob, the victim, decides to deposit 1 WETH (1e18 wei) into the contract, expecting to receive 1e18 shares, based on the formula: ( (1 share * 1e18 wei) / 1 wei )
- Before Bob’s transaction is finalized, Alice front-runs his transaction. She directly transfers 1 WETH to the contract.
- Now, when Bob’s deposit is calculated, the calculation results in:
( (1 share * 1e18 wei) / (1e18 wei + 1 wei) )
Because the denominator is larger than the numerator, division truncation occurs, and Bob receives 0 shares for his 1 WETH deposit.
- Alice’s 1 share is now worth 2 WETH + 1 wei, and Bob has lost his deposit.
Now that we’ve gone over the basics of share inflation, I’ll leave you with one last aside on this vulnerability. The above example primarily focused on the direct manipulation of the total assets in a contract. However, it is possible that an attacker is unable to directly transfer assets to a contract, but is still able to manipulate the total balance. A hypothetical scenario where indirect manipulation could occur is if an attacker is able to directly increase the amount of rewards granted to the contract. This would have the same effect of arbitrarily increasing the denominator independent of the numerator.
The second vulnerability shared among ERC-4626 compliant vault contracts, non ERC-4626 vault contracts, and vault-like contracts involves rounding, which is a problem that plagues many smart contracts. Specifically, for the three categories of vault contracts, rounding becomes an issue when depositing and withdrawing assets favors the user. Favorable rounding means that users receive more shares relative to the amount of assets that they deposited, and they receive more assets relative to the amount of shares that they redeem. This occurs when developers do not round in the correct direction relative to the function called by a user.
Rounding Up or Down
The ERC-4626 standard is very specific about which way to round when performing various operations. Paraphrased from the ERC-4626 standard, the specified rounding directions are as follows:
- If (1) the function is calculating how many shares to issue to a user for a certain amount of the underlying tokens they provide or (2) it’s determining the amount of the underlying tokens to transfer to them for returning a certain amount of shares, it should round down.
- If (1) the function is calculating the amount of shares a user has to supply to receive a given amount of the underlying tokens or (2) it’s calculating the amount of underlying tokens a user has to provide to receive a certain amount of shares, it should round up.
Unfortunately, the wording of the rounding specification from ERC-4626 is confusing, making it easy for developers who are coding vault implementations to misinterpret it. Let’s go over an example of favorable rounding:
In the above code snippet from our contract, we see that users can specify the amount of underlying asset they would like to withdraw. The function then uses a calculation to determine the corresponding amount of shares to subtract from the user. However, by rounding down, the function presents an opportunity for an attacker to manipulate the contract. If the attacker supplies an amount of asset they would like to withdraw such that the calculated shares amount is rounded down to 0, then they have effectively withdrawn assets for free (remember, Solidity rounds to 0 if the numerator is smaller than the denominator).
Now let’s walk through a scenario using the above code snippet with real numbers:
- Alice, the attacker, and Bob, the victim, have both deposited 1 WETH into a new contract. Alice and Bob both have 1e18 shares. Note that shares use the same order of magnitude as the underlying asset, and WETH has 18 decimal places.
- The contract receives 1 WETH in rewards, which is directly transferred to the contract. The total assets in the contract are now 3 WETH.
- Alice decides to withdraw 1 wei of WETH from the contract. The contract calculates the amount of shares to subtract from Alice using the following calculation:
(1 wei * 2e18 shares) / (3e18 wei)
Because the numerator is less than the denominator, 0 shares will be subtracted from Alice’s account.
- Alice will receive 1 wei of WETH, but will not have any shares subtracted from her balance in the contract. Alice could repeat this process over and over again to drain the contract of its funds.
Notice in the above example that the denominator had to change independently of the numerator as a prerequisite for this attack. If the total supply of shares (numerator) and the total amount of assets (denominator) are the same, then it would not be possible for division truncation to occur. As mentioned previously, having the denominator move independently of the numerator is something that all ERC-4626 compliant, non ERC-4626, and vault-like contracts must be able to do in order to reward depositors. Additionally, Alice could have moved the denominator herself by directly transferring WETH to the contract. However, Alice would only be able to steal back 1 wei of WETH at a time, making this attack very unlikely to occur on a real contract due to the fact that the gas cost of this attack vector typically outweighs the rewards from the exploit. Alice could directly transfer a large amount of WETH in order to make the attack more profitable, but this would require orders of magnitude more WETH (100, 1,000, 10,000, etc.) which further limits the viability of this exploit.
There is another consequence of this rounding behavior. When Bob attempts to withdraw his expected total amount of assets, the contract will encounter an integer underflow and revert. This is because Alice has removed assets without having her shares amount decremented, causing a deficit in the contract.
In the second part of our three part deep-dive, we went over two vulnerabilities that are shared among the three identified vault categories. Stay tuned for the final part of our deep-dive, where we’ll go over several proposed effective and ineffective mitigations for share inflation, and cover the mitigation for incorrect rounding.