Skip to content
Quantix Security Labs
Primer

ERC-4626 vault inflation attacks, and the defences that hold

An empty tokenised vault can be manipulated so that the next depositor receives no shares. Why the arithmetic allows it, and how virtual shares, dead shares and slippage checks close the gap.

Written by
Quantix Research, DeFi Security
Published
Reading time
6 minutes
Category
DeFi Security

Primer. Explains a known class of issue. Code is simplified and illustrative, not for production use.

Illustration: a vault whose assets are inflated by a direct donation, so one depositor holds a single share and the next is minted zero shares.
Contents
  1. Share accounting in one formula
  2. The attack, step by step
  3. Why rounding is the lever
  4. When a vault is exposed
  5. Defences that hold
  6. How much each defence protects
  7. What integrators should check
  8. Conclusions
  9. Recommendations
  10. Bibliography

ERC-4626 standardised the interface of tokenised vaults: deposit an asset, receive shares, redeem shares for a proportional amount of the asset. The standard made vaults composable, and it also made one of their oldest weaknesses easy to recognise. An empty or nearly empty vault can be manipulated so that the next depositor receives far fewer shares than their deposit is worth, sometimes none at all.

This primer explains the attack with the arithmetic that drives it, the conditions under which a vault is exposed, and the defences that hold, with numbers computed for each.

Share accounting in one formula

A vault prices its shares by dividing what it holds by what it has issued. When a user deposits assets, the vault mints

shares = assets × totalSupply / totalAssets

and when a user redeems, it pays out assets = shares × totalAssets / totalSupply. Integer division discards the remainder. EIP-4626 requires implementations to round in the vault’s favour: down when converting assets to shares on deposit, up when computing the assets a user must supply for a given number of shares. That rule protects existing holders from being diluted by rounding, and it is also what an attacker exploits.

NaiveVault.sol (illustrative)solidity
// Simplified, illustrative vault. Not production code.contract NaiveVault is ERC20 {    IERC20 public immutable asset;     function totalAssets() public view returns (uint256) {        return asset.balanceOf(address(this));   // includes direct donations    }     function deposit(uint256 assets, address receiver) external returns (uint256 shares) {        uint256 supply = totalSupply();        shares = supply == 0 ? assets : (assets * supply) / totalAssets();  // rounds down        asset.transferFrom(msg.sender, address(this), assets);        _mint(receiver, shares);                                             // may mint 0    }}

Two details in this sketch matter. totalAssets() reads the vault’s token balance, so tokens sent directly to the vault, without calling deposit, raise the share price for everyone. And nothing stops deposit from minting zero shares.

The attack, step by step

First-depositor inflation attack on an unprotected ERC-4626 vaultStep 1: the attacker deposits 1 wei and receives 1 share. Step 2: the attacker transfers 10,000 tokens directly to the vault, so one share is worth about 10,000 tokens. Step 3: a victim deposits 10,000 tokens and, because 10,000 times 1 divided by 10,000 plus 1 wei rounds down, receives zero shares. Step 4: the attacker redeems the only share and takes all 20,000 tokens.1 Attacker deposits 1 weitotalAssets1 weitotalSupply1 shareresult1 wei per share2 Attacker donates 10,000totalAssets10,000 + 1 weitotalSupply1 shareresult≈ 10,000 per share3 Victim deposits 10,000totalAssets20,000 + 1 weitotalSupply1 shareresultvictim minted 04 Attacker redeems 1totalAssets0totalSupply0 sharesresultattacker takes 20,000shares = assets × totalSupply / totalAssets = 10,000 × 1 / (10,000 + 1 wei) → 0Rounding down in convertToShares turns a direct donation into a transfer from the next depositor. Amounts in whole tokens.
The classic sequence against an unprotected vault. Amounts are in whole tokens; the arithmetic is shown in the box below the steps.
  1. The vault is empty. The attacker deposits 1 wei of the asset and receives 1 share.
  2. The attacker transfers 10,000 tokens directly to the vault. No shares are minted, so the single share is now backed by 10,000 tokens and 1 wei.
  3. A victim deposits 10,000 tokens. The vault computes 10,000 × 1 / (10,000 + 1 wei), which is just below 1 and rounds down to 0. The victim’s tokens are taken and no shares are minted.
  4. The attacker redeems the only share and receives all 20,000 tokens and 1 wei.

In practice the attacker front-runs the victim’s deposit, which is visible in the public mempool, or simply positions the vault and waits. The donation does not need to be large in absolute terms, only at least as large as the deposit being targeted. Partial versions of the attack, in which the victim receives some shares but fewer than fair, work with smaller donations.

Why rounding is the lever

The loss a depositor suffers from rounding is at most the value of one share. In a healthy vault with millions of shares outstanding, one share is worth very little, and rounding is noise. The attack works by making one share very valuable: with a total supply of 1, a single share carries the entire balance of the vault, and rounding away a fraction of a share means rounding away a fraction of everything.

Every defence below works by making that impossible, either by guaranteeing that shares never become individually valuable, or by refusing transactions in which rounding destroys value.

When a vault is exposed

The attack requires a specific set of conditions. A vault is exposed when all of these hold:

  • its total supply can be very small, which in practice means at or shortly after deployment, or after everyone has withdrawn;
  • its share price can be raised without minting shares, usually because totalAssets() reads a balance that anyone can increase by transfer;
  • deposits can mint zero or very few shares without reverting;
  • depositors, or the contracts that deposit on their behalf, do not check how many shares they received.

The second condition is broader than it looks. Strategies that count rewards, rebasing balances or assets held in another protocol can all be inflated by someone who sends assets to the right place.

Defences that hold

Virtual shares and a decimal offset. OpenZeppelin’s ERC-4626 implementation adds a virtual supply and a virtual asset to every conversion, so the vault behaves as if it always held a small position no one can redeem. With an offset of d, the virtual supply is 10^d shares. An attacker’s donation is then shared with those virtual shares, and most of it is lost to the attacker.

Virtual shares in OpenZeppelin ERC4626solidity
// OpenZeppelin Contracts 5.x: conversions use virtual shares and assets.function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256) {    return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding);} // A vault opts in to a larger offset by overriding one function.contract SafeVault is ERC4626 {    constructor(IERC20 asset_) ERC20("Safe Vault", "sVLT") ERC4626(asset_) {}     function _decimalsOffset() internal pure override returns (uint8) {        return 6; // shares carry 6 more decimals than the asset    }}

Dead shares. The deployer makes an initial deposit and sends the resulting shares to an address no one controls, or the vault mints a minimum amount to itself on the first deposit. The effect is similar to virtual shares, at the cost of real capital that is permanently locked.

Internal accounting. If totalAssets() tracks deposits, withdrawals and harvested yield in storage rather than reading balanceOf, a direct transfer does not change the share price. The donation step fails. This is a strong defence, but it requires every source of yield to be accounted for explicitly.

Slippage checks. A deposit that specifies a minimum number of shares, and reverts if fewer are minted, cannot be silently rounded to zero. This protects the depositor whatever the vault does.

Router-style minimum shares check (illustrative)solidity
// Depositor-side protection: refuse a deposit that mints too few shares.function depositWithMinShares(IERC4626 vault, uint256 assets, uint256 minShares, address receiver)    external    returns (uint256 shares){    IERC20(vault.asset()).safeTransferFrom(msg.sender, address(this), assets);    IERC20(vault.asset()).forceApprove(address(vault), assets);    shares = vault.deposit(assets, receiver);    require(shares >= minShares, "slippage");}

How much each defence protects

The table applies OpenZeppelin’s conversion formula to the scenario above: the attacker deposits 1 wei, donates 10,000 tokens, and a victim then deposits 10,000 tokens. Values are computed exactly and rounded for display.

VaultVictim’s sharesVictim redeemsVictim lossAttacker profit
No protection0010,000+10,000
Virtual shares, offset 016,666.673,333.33−3,333.33
Virtual shares, offset 31,0009,995.005.00−9,990.00
Virtual shares, offset 61,000,0009,999.9950.005−9,999.99
Outcome of the same attack under different decimal offsets (18-decimal asset, amounts in whole tokens)

Two conclusions follow. Even an offset of zero makes the attack unprofitable, because the attacker’s donation is shared with the virtual share; but the victim still loses a third of the deposit, so an attacker willing to destroy value can grief depositors. Larger offsets reduce the victim’s loss roughly by a factor of ten per decimal, at the cost of share amounts that look unusual to users. Combined with a minimum-shares check, a modest offset leaves nothing to exploit.

What integrators should check

Protocols that deposit into third-party vaults on behalf of users, such as aggregators, routers and lending markets that accept vault shares as collateral, inherit the risk. They should treat the number of shares received as an output to verify, not a given, and should be wary of vaults with very small supply. Integrators that price vault shares as collateral face the mirror image of this problem: a share price that anyone can raise by donation is a price an attacker can raise before borrowing. That is the same shape as the price manipulation described in our oracle analysis.

The vault share price manipulation case study and the Quillon Vaults research assessment show how this class of issue is reproduced and reported in a review.

Conclusions

The inflation attack is a rounding error made large. It needs a tiny supply, a share price that can be raised by donation, and deposits that may mint too few shares. Removing any one of those conditions defeats it. Virtual shares with a decimal offset remove the first in effect, internal accounting removes the second, and slippage checks remove the third.

The most robust vaults use more than one: a virtual offset so that no share is ever individually valuable, and an interface that lets depositors state the minimum they will accept.

Recommendations

Vault developers

  1. Build on an implementation with virtual shares, and choose a _decimalsOffset() that makes griefing uneconomic for your expected deposit sizes.
  2. Revert when a deposit would mint zero shares, and when a redemption would pay zero assets.
  3. Prefer internal accounting of totalAssets() where every yield source can be tracked explicitly.

Integrators and routers

  1. Expose a minimum-shares or maximum-assets parameter on every deposit and redemption path.
  2. Treat vault share prices as manipulable inputs when valuing collateral, and bound them against an independent price.

Reviewers

  1. Write a test that performs the four-step sequence against the vault at zero supply and after a full withdrawal.
  2. Check every path by which assets can reach the vault without minting shares.

Bibliography

Primary sources: standards, official documentation, incident reports and papers.

  1. 1EIP-4626: Tokenized Vaults(opens in a new tab)Ethereum Improvement Proposals
  2. 2ERC-4626 (inflation attack and virtual offset)(opens in a new tab)OpenZeppelin Contracts 5.x documentation
  3. 3A Novel Defense Against ERC4626 Inflation Attacks(opens in a new tab)OpenZeppelin
  4. 4SoK: Decentralized Finance (DeFi) Attacks(opens in a new tab)Zhou et al., arXiv:2208.13035

Applied on this site

Assessments, case studies and tools that apply this publication.

Want this question asked of your code?

Quantix reviewers apply the same reasoning to production systems. Share what you are building and when it ships.