Skip to content

Vaults

ERC4626 is the implementation of a standard API for tokenized Vaults representing shares of a single underlying EIP-20 token.

Even though the latest Open Zeppelin ERC4626 implementation has taken some precautions about inflation attack, it is important to analyze because it is a critical aspect of security for tokenized vaults.

What the attack really needs is a large change in totalAssets/totalSharestotalAssets/totalShares relative to the vault’s current size; that’s easier when the vault is empty, but “empty” here just means “low liquidity,” not strictly zero.

If the vault has modest liquidity (e.g., early stages), the attacker can still front‑run a victim by:

  • acquiring some shares (possibly with a non‑trivial deposit),
  • making a donation large compared to existing assets,
  • then letting the victim deposit at the inflated price so the victim receives fewer shares and the attacker exits later at a profit
function convertToShares(uint256 assets) public view returns (uint256) {
if (totalAssets() == 0) return assets;
return totalSupply() * assets / totalAssets();
}
function convertToAssets(uint256 shares) public view returns (uint256) {
return totalAssets() * shares / totalSupply();
}
StepActionTotal AssetsTotal SharesNotes
1.Vault is empty.00Vulnerable new vault.
2.Attacker deposits 0.1 token.0.1 token1 shareAttacker owns 100% of shares.
3.Frontrun: Donates 100 tokens (no shares minted).100.1 tokens1 shareInflates asset value per share.
4.Victim deposits 100 tokens200.1 tokens1 shareshares = 100/100.1 → 0.99 (rounds down). Victim gets 0 shares.
5.Attacker redeems 1 share, takes entire vault balance.00Attacker gets 200.1 tokens. Steals victim’s 100 tokens (100% steal).

Vault adds a condition that will deny zero shares.

It makes the attack harder and less profitable.

function deposit(uint256 assets) public {
require(convertToShares(assets) != 0);
//...
}
StepActionTotal AssetsTotal SharesNotes
1.Vault is empty.00Vulnerable new vault.
2.Attacker deposits 0.1 token.0.1 tokens1 shareAttacker owns 100% of shares.
3.Frontrun: Donates 500 tokens (no shares minted).500.1 tokens1 shareInflates asset value per share.
4.Victim deposits 1000 tokens1500.1 tokens2 sharesshares = 1000/500.1 → 1.99 (rounds down). Victim gets 1 share.
5.Attacker redeems 1 share, takes 50% vault balance.750.05 tokens1 shareAttacker gets 750.05 tokens. Steals victim’s 250 tokens (25% steal).

Vault adds a condition that will burn X number of shares from the first deposit.

It makes attack even harder, plus hacker can no longer steal funds.

The initial deposit should be big enough to mitigate for this loss.

uint constant NUMBER_OF_DEAD_SHARES = 100;
function deposit(uint256 assets) public {
asset.transferFrom(msg.sender, address(this), assets);
uint shares = convertToShares(assets);
if (totalShares() == 0) {
_mint(address(0), NUMBER_OF_DEAD_SHARES);
shares -= NUMBER_OF_DEAD_SHARES;
}
_mint(msg.sender, shares);
}
StepActionTotal AssetsTotal SharesNotes
1.Vault is empty.00Vulnerable new vault.
2.Attacker deposits 100 token.100 tokens0 shareAttacker owns 100-100=0. 0% of shares.
3.Frontrun: Donates 2000 tokens (no shares minted).2100 tokens0 shareAttacker burns 2000 tokens.
4.Victim deposits 2000 tokens2100 tokens0 sharesshares = 2000/2100 → 0.95 (rounds down). Victim gets 0 shares.
5.Attacker cannot reddeem.2100 tokens0 shareAttacker burns 100% of victim’s deposit and looses 2000 tokes.

Vault in increases the shares accuracy by multiplying with a big number.

This is the implementation that OZ ERC4626 currently uses, but you must manually override the _decimalsOffset function to increase the precision.

function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256) {
return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding);
}
function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual returns (uint256) {
return shares.mulDiv(totalAssets() + 1, totalSupply() + 10 ** _decimalsOffset(), rounding);
}
// WE HAVE OVERRIDDEN HIS FUNCTION TO INCREASE PRECISION
function _decimalsOffset() internal view virtual returns (uint8) {
return 6;
}
StepActionTotal AssetsTotal SharesNotes
1.Vault is empty.00Vulnerable new vault.
2.Attacker deposits 1 token.1 token1 shareAttacker owns 100% of shares.
3.Frontrun: Donates 1000000 tokens (no shares minted).1000001 tokens1 shareInflates asset value per share.
4.Victim deposits 1 tokens1000002 tokens1 shareshares = 1/1000001 → 0.999999 (rounds down). Victim gets 0 shares.
5.Attacker redeems 1 share, takes entire vault balance.00Attacker gets 1000002 tokens. Steals victim’s 1 tokens and uses 1000001 tokens.

The more you increase the decimal offset, the more expensive it is for the attacker.

  • Client should revert if the amount received is not within a slippage tolerance regardless.