Dev.to Security 🔐 Cybersecurity 👁 0 📖 2 min read

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

  1. Attacker deposits 1 wei. The vault mints them 1 share. Now total_shares = 1.
  2. Attacker "donates" a large amount directly to the vault — a raw transfer of, say, 10,000 tokens straight to the vault address, not through deposit(). Now total_assets = 10,000e18 + 1, but total_shares is still 1.
  3. One share is now worth ~10,000 tokens.
  4. 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).
  5. 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)) as total_assets lets anyone inflate it with a direct transfer (a "donation"), bypassing deposit().
  • 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 ERC4626 as your base (it ships the _decimalsOffset() mitigation).
  • If you roll your own, don't derive totalAssets from raw balanceOf — 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).

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.