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.

Contents
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.
// 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
- The vault is empty. The attacker deposits 1 wei of the asset and receives 1 share.
- 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.
- 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. - 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.
// 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.
// 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.
| Vault | Victim’s shares | Victim redeems | Victim loss | Attacker profit |
|---|---|---|---|---|
| No protection | 0 | 0 | 10,000 | +10,000 |
| Virtual shares, offset 0 | 1 | 6,666.67 | 3,333.33 | −3,333.33 |
| Virtual shares, offset 3 | 1,000 | 9,995.00 | 5.00 | −9,990.00 |
| Virtual shares, offset 6 | 1,000,000 | 9,999.995 | 0.005 | −9,999.99 |
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
- Build on an implementation with virtual shares, and choose a
_decimalsOffset()that makes griefing uneconomic for your expected deposit sizes. - Revert when a deposit would mint zero shares, and when a redemption would pay zero assets.
- Prefer internal accounting of
totalAssets()where every yield source can be tracked explicitly.
Integrators and routers
- Expose a minimum-shares or maximum-assets parameter on every deposit and redemption path.
- Treat vault share prices as manipulable inputs when valuing collateral, and bound them against an independent price.
Reviewers
- Write a test that performs the four-step sequence against the vault at zero supply and after a full withdrawal.
- Check every path by which assets can reach the vault without minting shares.
Bibliography
Primary sources: standards, official documentation, incident reports and papers.
- 1EIP-4626: Tokenized Vaults(opens in a new tab)Ethereum Improvement Proposals
- 2ERC-4626 (inflation attack and virtual offset)(opens in a new tab)OpenZeppelin Contracts 5.x documentation
- 3A Novel Defense Against ERC4626 Inflation Attacks(opens in a new tab)OpenZeppelin
- 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.







