ERC-4626 first-depositor inflation, explained for founders
If you're shipping an ERC-4626 vault, there's one attack you need to understand before mainnet — because it targets your very first real depositor, and it's caused by a rounding detail that's easy to miss. It's called t
If you're shipping an ERC-4626 vault, there's one attack you need to understand before mainnet — because it targets your very first real depositor, and it's caused by a rounding detail that's easy to miss.
It's called the first-depositor inflation attack (or "empty-vault share inflation"). Here's how it works, in plain English.
The setup: shares vs. assets
An ERC-4626 vault issues shares representing a claim on the underlying assets. The exchange rate is roughly:
shares_you_get = deposit_amount * total_shares / total_assets
When the vault is brand new, total_shares and total_assets are both zero, and the first deposit sets the initial rate. That empty starting state is the vulnerability.
The attack, step by step
-
Attacker deposits 1 wei. The vault mints them 1 share. Now
total_shares = 1. -
Attacker "donates" a large amount directly to the vault — a raw
transferof, say, 10,000 tokens straight to the vault address, not throughdeposit(). Nowtotal_assets = 10,000e18 + 1, buttotal_sharesis still1. - One share is now worth ~10,000 tokens.
-
The victim deposits 10,000 tokens expecting a fair share. The share math rounds down:
shares = 10,000e18 * 1 / 10,000e18 ≈ 1, but integer rounding gives them 0 shares (or far fewer than they paid for). - The attacker now owns a share worth the victim's deposit. They redeem and walk away with the victim's money.
The victim did nothing wrong. They deposited into a vault whose accounting could be manipulated because it trusted balanceOf(address(this)) and started from empty.
Why it happens
Two ingredients:
-
Accounting from raw balance. Using
token.balanceOf(address(this))astotal_assetslets anyone inflate it with a direct transfer (a "donation"), bypassingdeposit(). - Rounding that favors the vault/attacker on the first deposit, when supply is tiny.
The fix: virtual shares & assets (decimal offset)
OpenZeppelin's ERC-4626 implementation defends against this with virtual shares and virtual assets — it adds a small virtual offset to both sides of the ratio so the vault never really starts from zero, and rounding always favors the vault over the attacker rather than the reverse. In practice:
- Use OpenZeppelin's
ERC4626as your base (it ships the_decimalsOffset()mitigation). - If you roll your own, don't derive
totalAssetsfrom rawbalanceOf— track deposited assets internally, or add the virtual-offset defense. - Consider seeding the vault with a tiny initial deposit at deployment (a "dead shares" burn) so it's never empty for the first real user.
- Add an invariant/fuzz test: "a depositor never receives 0 shares for a non-trivial deposit," and "no sequence of donate+deposit lets one account extract another's assets."
Solmate's minimal ERC-4626, by contrast, intentionally omits this — it leaves the defense to the integrator. That's a valid library choice, but it means you have to add it. (My scanner flags exactly this difference between the two.)
Check your vault in one command
OpenClaw Audit has a dedicated first-depositor-inflation detector, plus checks for donation-based accounting (balanceOf(address(this))) and ERC-4626 rounding direction:
pipx run --spec git+https://github.com/juan23z/openclaw-audit openclaw-audit <your-vault-repo>
Heuristic candidates — verify before acting. But for a vault, this is one of the first things to rule out.
Building an ERC-4626 vault and want a human to check the share math before you ship? A hand-verified Quick Scan is $49 (one contract, 48h, and if it's not useful you don't pay).
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.