Shares are minted as assets * totalSupply / totalAssets with no virtual shares/assets offset and no minimum initial deposit. The first depositor mints 1 wei of shares, donates a large amount directly to the vault to inflate the share price, and every subsequent depositor's deposit rounds down to zero shares — their funds are captured by the attacker.
// 1. attacker deposit(1) -> mints 1 share, totalAssets = 1 // 2. attacker token.transfer(vault, 1e18) // donate, price/share = ~1e18 // 3. victim deposit(1e18) -> shares = 1e18*1/(1e18+1) = 0 (rounds down) // 4. attacker redeem(1 share) -> takes victim's 1e18
// OpenZeppelin ERC4626 virtual-offset, or seed dead shares
function deposit(uint256 assets) external returns (uint256 shares) {
shares = assets.mulDiv(totalSupply() + 10**_decimalsOffset(), totalAssets() + 1, Math.Rounding.Floor);
require(shares > 0, "zero shares");
_mint(msg.sender, shares);
}
Largest recorded losses in the same failure class — related, not the same bug:
The SaferICO scanner runs 201 detectors over your Solidity source, SAFE-0111 among them. Paste an address or the source itself — a small per-scan fee, shown before you sign, or unlimited on any plan.