Vault Inflation Attack (ERC-4626 First Depositor)
Detects ERC-4626 tokenized vault implementations that are vulnerable to the first-depositor share price inflation attack, where an attacker manipulates the share price to cause subsequent depositors to receive zero shares.
Vault Inflation Attack (ERC-4626 First Depositor)
Overview
Remediation Guide: How to Fix Vault Inflation Attacks
The vault inflation detector identifies ERC-4626 tokenized vault implementations that are vulnerable to the first-depositor share inflation attack. In this attack, an adversary exploits integer rounding in share price calculations by: (1) becoming the first depositor with a negligible amount, (2) directly transferring a large amount of the underlying asset to the vault address without calling deposit(), and (3) causing subsequent legitimate depositors to receive zero shares due to rounding down.
Sigvex detects the vulnerability by identifying contracts that expose ERC-4626 function selectors (deposit(uint256,address) 0x6e553f65, mint(uint256,address) 0x94bf804d, withdraw(uint256,address,address) 0xb460af94, redeem(uint256,address,address) 0xba087652, totalAssets() 0x01e1d114) and contain division operations in share calculation functions (deposit, mint, convertToShares, previewDeposit, previewMint) without virtual share offsets or minimum deposit guards. The detector reduces confidence for contracts using OpenZeppelin ERC-4626 (v4.9+), Solmate, or Solady, which include built-in inflation protection.
Why This Is an Issue
The ERC-4626 standard computes a new depositor’s share count as:
shares = assets * totalSupply / totalAssets
When totalSupply is 1 (one share from the attacker’s initial deposit) and totalAssets has been inflated to 1,000,000 tokens via a direct transfer donation:
shares = (1 * 1) / 1,000,000 = 0 (rounded down)
A depositor who sends 999,999 tokens receives zero shares. The attacker, holding the only share, can then call redeem() to withdraw the entire vault balance including the victim’s deposit.
The attack is profitable when:
- The donated amount > attacker’s initial deposit
- The victim’s deposit (rounded down) > transaction costs
At scale — when many users have deposited small amounts that all round to zero — the attacker drains all of them in a single redeem() call.
OpenZeppelin’s ERC-4626 added virtual share protection in v4.9.0 (2023). Vaults written against earlier versions or custom implementations remain vulnerable.
How to Resolve
// Before: Vulnerable — share calculation rounds to zero for small deposits
// after an attacker inflates the share price
contract VulnerableVault is ERC4626 {
constructor(IERC20 asset) ERC4626(asset) ERC20("VaultToken", "vTKN") {}
// Inherits convertToShares: assets * totalSupply / totalAssets
// No protection against donation-based inflation
}
// After: Add virtual shares/assets offset (OZ v4.9+ pattern)
contract SecureVault is ERC4626 {
// Offset shifts the share price so that 1 donated token cannot inflate
// the price enough to round a reasonable deposit to zero
uint8 public constant DECIMALS_OFFSET = 3; // 10^3 = 1000x virtual shares
constructor(IERC20 asset) ERC4626(asset) ERC20("VaultToken", "vTKN") {}
function _decimalsOffset() internal pure override returns (uint8) {
return DECIMALS_OFFSET;
}
// With offset: convertToShares uses (totalSupply + 10^offset) / (totalAssets + 1)
// A donation of D tokens inflates price to only D / 10^offset per initial share
// Making the attack unprofitable for any reasonable DECIMALS_OFFSET
}
Examples
Vulnerable Code
import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
// Vulnerable: no virtual offset, no minimum deposit
contract SimpleVault is ERC4626 {
constructor(IERC20 asset)
ERC4626(asset)
ERC20("SimpleVault", "sVLT")
{}
// Uses default convertToShares: assets * totalSupply / totalAssets
// Attack sequence:
// 1. Attacker: deposit(1 wei) → receives 1 share, totalSupply=1, totalAssets=1
// 2. Attacker: asset.transfer(vault, 1_000_000e18) → totalAssets=1_000_000e18+1
// 3. Victim: deposit(999_999e18) → shares = 999_999e18 * 1 / 1_000_000e18 = 0
// 4. Victim receives 0 shares, 999_999e18 tokens lost to vault
// 5. Attacker: redeem(1 share) → withdraws everything
}
Fixed Code
import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
// Fixed: virtual share offset makes the attack unprofitable
contract SecureVault is ERC4626 {
// 10^3 virtual shares means attacker would need to donate 10^3x
// the victim's deposit to round shares to zero — economically infeasible
uint8 private constant _OFFSET = 3;
constructor(IERC20 asset)
ERC4626(asset)
ERC20("SecureVault", "sVLT")
{}
function _decimalsOffset() internal pure override returns (uint8) {
return _OFFSET;
}
}
// Alternative: Dead shares pattern (burn initial shares to address(0))
contract DeadSharesVault is ERC4626 {
bool private _initialized;
constructor(IERC20 asset)
ERC4626(asset)
ERC20("DeadVault", "dVLT")
{}
// On first deposit, mint a portion of shares to address(0) to anchor the price
function initialize(uint256 seedAmount) external {
require(!_initialized, "Already initialized");
_initialized = true;
// Transfer seed deposit and mint dead shares
IERC20(asset()).transferFrom(msg.sender, address(this), seedAmount);
_mint(address(0xdead), seedAmount); // Dead shares anchor the price
}
}
Sample Sigvex Output
{
"detector_id": "vault-inflation",
"severity": "high",
"confidence": 0.70,
"description": "Contract SimpleVault implements ERC-4626 selectors (deposit, withdraw, totalAssets) and contains division operations in share calculation functions without virtual share offsets or minimum deposit guards. An attacker can inflate the share price via a direct transfer donation to cause subsequent depositors to receive zero shares.",
"location": { "function": "convertToShares(uint256)", "offset": 0 }
}
Detection Methodology
Sigvex identifies vault inflation vulnerabilities through the following steps:
- ERC-4626 identification: Scans bytecode for ERC-4626 function selectors. Requires at least two of:
deposit(0x6e553f65),mint(0x94bf804d),withdraw(0xb460af94),redeem(0xba087652),totalAssets(0x01e1d114). Alternately, falls back to IR function name matching when bytecode is unavailable. - Division pattern scan: Searches all functions (particularly those named
deposit,mint,convertToShares,previewDeposit,preview*) for division (DIV) operations and multiplication-then-division sequences (MULimmediately followed byDIV). Up to three division locations are reported. - Virtual offset check (confidence modifier): Contracts that use OpenZeppelin’s
_decimalsOffset()override, Solmate’s ERC4626, or Solady’s ERC4626 have built-in inflation protection. Detected via library metadata in theEvmDetectionContextblackboard. Findings are down-graded to Low severity with 0.25x confidence when any of these libraries is present.
Base confidence: 0.70. Reduced to 0.25x when audited library vault detected.
Limitations
False positives:
- Vaults that implement a custom virtual offset mechanism not using the OZ
_decimalsOffset()pattern may be flagged because the library detection heuristic does not recognize the custom pattern. - Vaults with a governance-controlled initialization that seeds the pool with a large amount before opening to the public may be flagged even though the economic attack surface is negligible.
False negatives:
- Proxy vault implementations where the logic contract uses OZ ERC4626 but is not identified as such from the proxy bytecode alone may receive full confidence findings that are false.
- Vaults that implement a minimum deposit amount check only via a
requireon the input (not the share calculation) may pass without the check being recognized.
Related Detectors
- Reward Manipulation — related flash loan pool manipulation patterns
- Precision Errors — division-before-multiplication precision loss
- Flash Loan — flash loans can be used to seed the initial deposit in some variants