ERC-4626 is the tokenized vault standard — the foundation of most DeFi yield strategies, lending protocols, and liquid staking wrappers. Its share-based accounting introduces a class of vulnerability that has affected multiple production vaults: the share inflation attack, also called the donation attack or first depositor problem.
This post explains how the attack works mathematically, what real protocols have lost to it, and the mitigations that actually work.
How ERC-4626 Share Accounting Works
ERC-4626 vaults issue shares representing a proportional claim on the vault's assets. The exchange rate between shares and assets is:
shares = assets × totalShares / totalAssets
assets = shares × totalAssets / totalShares
When the vault is empty (totalShares = 0, totalAssets = 0), a special case applies — usually the first depositor gets shares 1:1 with their asset deposit.
The Share Inflation Attack
Setup: Empty vault (totalShares = 0, totalAssets = 0)
Step 1: Attacker deposits 1 wei
→ Attacker receives 1 share
→ totalShares = 1, totalAssets = 1
Step 2: Attacker DONATES (directly transfers) 1,000,000 assets to the vault address
→ totalShares = 1 (unchanged — donation doesn't mint shares)
→ totalAssets = 1,000,001 (donated assets inflate the price)
Step 3: Victim deposits 1,999,999 assets
→ shares = 1,999,999 × 1 / 1,000,001 = 1 share (rounds DOWN to 1)
→ Wait: 1,999,999 / 1,000,001 = 1.999... → rounds to 1
Step 4: Attacker redeems their 1 share
→ assets = 1 × (1,000,001 + 1,999,999) / 2 = 1,500,000
Attacker deposited: 1 (step 1) + 1,000,000 (step 2 donation) = 1,000,001
Attacker redeemed: 1,500,000
Profit: ~500,000 — stolen from the victim
The attack works because:
1. The donation inflates totalAssets without increasing totalShares
2. Rounding (integer division rounds down) causes the victim to receive fewer shares than they should
3. The attacker's 1 share now represents a larger fraction of the vault than they paid for
Vulnerable Implementation
// VULNERABLE: naive ERC-4626 with integer division rounding
contract VulnerableVault is ERC4626 {
constructor(IERC20 asset) ERC4626(asset) ERC20("Vault", "vTKN") {}
// Standard convertToShares — vulnerable to share inflation
function convertToShares(uint256 assets) public view override returns (uint256) {
uint256 supply = totalSupply();
return supply == 0
? assets // first depositor: 1:1
: assets.mulDiv(supply, totalAssets(), Math.Rounding.Down);
}
}
Any vault that:
1. Starts at 0 shares / 0 assets
2. Uses Math.Rounding.Down (or equivalent) for share calculation
3. Allows direct asset transfers (donations) to the vault address
...is vulnerable to this attack class.
Real-World Incidents
Sonne Finance (2024): Share inflation attack on a Compound v2 fork. The attacker donated assets to manipulate the exchange rate before a new market was added. Lost ~$20M.
Multiple Compound v2 forks: The pattern predates ERC-4626 — Compound's cToken design has the same fundamental vulnerability. New markets are consistently targeted in the first few blocks after deployment.
Yearn v2 vault near-miss: Yearn's earlier vault implementations had the vulnerability and required emergency migration before exploitation.
The Mitigation: Virtual Shares (Dead Shares)
OpenZeppelin's ERC-4626 implementation (v4.9+) adds virtual offset shares and assets to prevent the inflation attack:
// OpenZeppelin's mitigation approach (simplified)
abstract contract ERC4626 is ERC20, IERC4626 {
// Virtual offset — effectively "pre-mints" shares to address(0)
function _decimalsOffset() internal view virtual returns (uint8) {
return 0; // Override to return e.g. 3 for 1000x virtual shares
}
function totalAssets() public view virtual returns (uint256) {
return _asset.balanceOf(address(this));
}
function convertToShares(uint256 assets) public view virtual returns (uint256) {
return assets.mulDiv(
totalSupply() + 10 ** _decimalsOffset(), // virtual shares
totalAssets() + 1, // virtual assets
Math.Rounding.Floor
);
}
function convertToAssets(uint256 shares) public view virtual returns (uint256) {
return shares.mulDiv(
totalAssets() + 1, // virtual assets
totalSupply() + 10 ** _decimalsOffset(), // virtual shares
Math.Rounding.Floor
);
}
}
How virtual shares prevent the attack:
With _decimalsOffset() = 3 (1000 virtual shares):
Initial state (empty vault):
totalSupply() = 0, but formula uses 0 + 1000 = 1000 virtual shares
totalAssets() = 0, but formula uses 0 + 1 = 1 virtual asset
Step 1: Attacker deposits 1 wei
→ shares = 1 × (0 + 1000) / (0 + 1) = 1000 shares
→ totalShares = 1000, totalAssets = 1 (real)
Step 2: Attacker donates 1,000,000 assets
→ totalShares = 1000 (virtual formula: 1000 + 1000 = 2000)
→ totalAssets = 1,000,001 (formula: 1,000,001 + 1 = 1,000,002)
Step 3: Victim deposits 1,999,999 assets
→ shares = 1,999,999 × (1000 + 1000) / (1,000,001 + 1) = 1,999,999 × 2000 / 1,000,002
→ ≈ 3,999,994 shares (not 1!)
Step 4: Attacker redeems 1000 shares
→ 1000 × 1,000,002 / (1000 + 3,999,994 + 1000) → attacker gets back only what they put in
The virtual shares dilute the attacker's ability to manipulate the exchange rate — they'd need to donate an astronomically large amount to move the ratio meaningfully.
Dead Shares Pattern
An alternative mitigation: mint dead shares to address(0) on deployment:
contract SafeVault is ERC4626 {
constructor(IERC20 asset) ERC4626(asset) ERC20("Vault", "vTKN") {
// Mint dead shares on deploy to prevent first-depositor manipulation
_mint(address(0), 1000);
// Also seed the vault with initial assets
asset.transferFrom(msg.sender, address(this), 1000);
}
}
Dead shares mean totalSupply is never 0, eliminating the special-case first depositor logic. The initial seeding ensures totalAssets also starts above 0.
Limitation: Burned shares represent a permanent small loss to the deployer, and the protection only scales with the size of the dead shares relative to the attacker's donation.
Rounding Direction Matters
ERC-4626 defines that convertToShares should round down (favoring the vault/protocol) and convertToAssets should also round down (favoring the vault). This is intentional: rounding should never benefit depositors at the expense of the protocol.
However, previewDeposit and previewMint may have different rounding to give users accurate expectations. The key is consistency — all share minting operations must round against the depositor, not in their favor.
// Safe rounding: deposit path rounds DOWN (gives user fewer shares)
function previewDeposit(uint256 assets) public view virtual returns (uint256) {
return convertToShares(assets); // rounds down
}
// Safe rounding: withdrawal path rounds UP (user must burn more shares)
function previewWithdraw(uint256 assets) public view virtual returns (uint256) {
return assets.mulDiv(
totalSupply() + 10 ** _decimalsOffset(),
totalAssets() + 1,
Math.Rounding.Ceil // rounds UP for withdrawals
);
}
Practical Mitigation Checklist
For new ERC-4626 vault implementations:
- [ ] Use OpenZeppelin's ERC4626 with _decimalsOffset() >= 3 for high-value vaults
- [ ] Or deploy with dead shares minted to address(0) plus initial seeding
- [ ] Ensure convertToShares rounds down (not up) for deposits
- [ ] Ensure convertToAssets rounds down for redemptions
- [ ] Test with an attacker who front-runs the first deposit with a large donation
For protocol integrators using ERC-4626 vaults:
- [ ] Verify the underlying vault uses virtual shares or dead shares
- [ ] Check that totalSupply() cannot reach 0 after initial deployment
- [ ] Do not integrate vaults that are freshly deployed with 0 initial liquidity without share inflation mitigation
Testing for the attack:
function test_shareInflationAttack() public {
// Fund attacker and victim
deal(address(asset), attacker, 2_000_001);
deal(address(asset), victim, 1_999_999);
// Attacker: deposit 1 wei
vm.prank(attacker);
vault.deposit(1, attacker);
// Attacker: donate 1M directly
vm.prank(attacker);
asset.transfer(address(vault), 1_000_000);
// Victim: deposit 1,999,999
vm.prank(victim);
vault.deposit(1_999_999, victim);
// Attacker: redeem
vm.prank(attacker);
uint256 attackerShares = vault.balanceOf(attacker);
vault.redeem(attackerShares, attacker, attacker);
// Verify attacker didn't profit at victim's expense
assertLe(
asset.balanceOf(attacker),
2_000_001, // attacker should not have more than they started with
"Share inflation attack succeeded"
);
}
Scanner Coverage
| Vulnerability | Slither | Mythril | Semgrep | AI |
|---|---|---|---|---|
| Missing virtual shares / dead shares | ❌ | ❌ | ❌ | ✅ |
| Rounding direction error | ❌ | ❌ | ❌ | ✅ |
| Zero totalSupply edge case | ⚠️ | ⚠️ | ❌ | ✅ |
| Direct asset transfer vulnerability | ❌ | ❌ | ❌ | ✅ |
Share inflation is a semantic vulnerability — the individual arithmetic operations are correct, but the combined behavior in an adversarial sequence is exploitable. AI analysis can detect the pattern by reasoning about whether a vault's share accounting is safe against front-running and donation attacks.
Scan your ERC-4626 vault with ContractScan — the AI engine checks for share inflation risks, rounding direction errors, and virtual share implementation in a single pass.
Related: Rug Pull Detection Patterns — donation attacks overlap with rug pull mechanics when the vault deployer controls the initial shares.
Related: Oracle Manipulation Attacks — share price manipulation is analogous to oracle price manipulation, both exploiting rate-based accounting.
Important Notes
This post is for informational and educational purposes only. It does not constitute financial, legal, or investment advice. The security analysis provided is based on available data and automated tools, which may not capture all potential vulnerabilities. Always conduct a professional audit before deploying smart contracts.