The first-depositor share inflation attack
A vault that issues shares has one moment of genuine fragility: when it is empty. The first deposit sets the exchange rate between shares and the underlying asset, and an attacker who controls that first deposit can set the rate so badly that the next person's money mints almost nothing. The victim's funds land in the vault. The shares that represent them do not.
This is the share inflation, or first-depositor, attack. It has drained real vaults, and it keeps reappearing because every line involved looks correct on its own.
The setup
When a vault is empty, it has no price to quote, so it bootstraps one. The usual rule: the first depositor receives shares equal to the assets they put in. After that, shares are minted in proportion to what is already there.
function deposit(uint256 assets) external returns (uint256 shares) {
shares = totalSupply == 0
? assets
: assets * totalSupply / totalAssets(); // totalAssets() reads token balance
_mint(msg.sender, shares);
token.transferFrom(msg.sender, address(this), assets);
}
The weakness is that totalAssets() reads the vault's token balance directly. Anyone can increase that balance by sending tokens to the vault, without going through deposit and without minting shares. That single fact is the whole exploit.
The attack, step by step
- The attacker deposits the smallest possible amount into the empty vault, 1 wei, and receives 1 share. Total supply is now 1.
- The attacker transfers a large amount, say 10,000 tokens, directly to the vault address. No shares are minted. The vault now holds 10,000 tokens backing a single share.
- A victim deposits 10,000 tokens through the front door. Their shares are
10,000 * 1 / 20,000 = 0after the division rounds down. They get zero shares for real money. - The attacker redeems their 1 share, now worth the entire balance, and walks away with both deposits.
The victim does not need to deposit exactly the wrong amount. Any deposit below the inflated share price rounds to zero, and amounts above it lose the remainder to the attacker. The donation step turns rounding, normally a dust-level concern, into a wholesale loss. It is the same arithmetic we wrote about in rounding direction, weaponized by controlling the denominator.
The rule
A vault's accounting must not be steerable by funds that arrived outside its accounting. The share price should depend on what was deposited, never on the raw token balance.
Three defenses hold, and the strongest deployments use more than one:
- Virtual shares and assets. Offset both sides of the ratio by a constant, as OpenZeppelin's ERC4626 does. The vault behaves as if a small fixed amount is always present, so the first depositor cannot own 100% of supply and the donation cannot move the price far. The attack becomes too expensive to be worth it.
- Internal accounting. Track deposited assets in a state variable and quote against that, not against
token.balanceOf(this). Donated tokens then sit outside the share math entirely. - Seed the vault. Have the deployer make the first deposit and lock those shares, so no attacker ever holds the first one.
How we test it
We write the attacker's path as a test and assert it fails to profit:
// attacker deposits 1 wei, donates a large sum, victim deposits normally
vault.deposit(1, attacker);
token.transfer(address(vault), 10_000e18); // direct donation
vault.deposit(10_000e18, victim);
assertGt(vault.balanceOf(victim), 0); // victim must receive shares
// and the attacker cannot redeem more than they put in
assertLe(vault.previewRedeem(vault.balanceOf(attacker)), 1 + donationRefundable);
We also fuzz the deposit and donation amounts, because a defense tuned for one ratio can still leak at another, and we check the round-trip invariant from the rounding note holds even after a donation.
Why it hides
The deposit function is textbook. The proportional minting is correct. The empty-vault branch is correct. The bug is not in any line but in the assumption that the only way to change the vault's balance is to deposit. Once you stop trusting the raw balance, the whole class closes, and until you do, every review of the individual lines comes back clean.
If you are shipping an ERC4626 vault or anything that mints shares against a balance, the empty-state behavior is one of the first things we pressure-test. Related: rounding direction is a security property.