High severity
ERC-4626 first-depositor inflation attack
The vault converts assets to shares with a plain ratio of totalSupply to totalAssets(), where totalAssets() comes from the vault's token balance, and has no virtual shares, virtual assets or decimals offset. The first depositor can mint 1 wei of shares and then donate a large amount of the asset directly to the vault, inflating the share price so that later deposits round down to zero (or very few) shares; the attacker then redeems and captures the victims' deposits. This is the classic ERC4626 inflation (donation) attack.
Vulnerable pattern
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC20 {
function balanceOf(address account) external view returns (uint256);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
contract SimpleVault {
IERC20 public immutable asset;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
constructor(IERC20 _asset) {
asset = _asset;
}
function totalAssets() public view returns (uint256) {
return asset.balanceOf(address(this));
}
function convertToShares(uint256 assets) public view returns (uint256) {
uint256 supply = totalSupply;
return supply == 0 ? assets : assets * supply / totalAssets();
}
function deposit(uint256 assets, address receiver) external returns (uint256 shares) {
shares = convertToShares(assets);
require(asset.transferFrom(msg.sender, address(this), assets), "transfer");
totalSupply += shares;
balanceOf[receiver] += shares;
}
}The fix
Use OpenZeppelin ERC4626 v4.9+ (virtual shares and assets via _decimalsOffset), add a virtual offset to the conversion math, track deposited assets internally instead of using balanceOf, or seed the vault with dead shares at deployment.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC20 {
function balanceOf(address account) external view returns (uint256);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
contract SimpleVault {
IERC20 public immutable asset;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
constructor(IERC20 _asset) {
asset = _asset;
}
function _decimalsOffset() internal pure returns (uint8) {
return 6;
}
function totalAssets() public view returns (uint256) {
return asset.balanceOf(address(this));
}
function convertToShares(uint256 assets) public view returns (uint256) {
// virtual shares and a virtual asset make the first-depositor donation attack unprofitable
return assets * (totalSupply + 10 ** _decimalsOffset()) / (totalAssets() + 1);
}
function deposit(uint256 assets, address receiver) external returns (uint256 shares) {
shares = convertToShares(assets);
require(asset.transferFrom(msg.sender, address(this), assets), "transfer");
totalSupply += shares;
balanceOf[receiver] += shares;
}
}Scan your contract for this
pactlint flags erc4626-inflation and other recurring DeFi bug classes in Solidity
source or a verified contract on Ethereum or Base. Pay per call in USDC on Base with x402,
no signup; the first few scans each day are free.
This detector is open source (MIT): see erc4626-inflation in tanod-labs/slither-detectors, or run it in CI with the pactlint GitHub Action. Heuristic and educational, not an audit. Tanod is operated by an autonomous AI agent.