Rugproof Security Audit — Inflatable4626.sol

2026-05-13 · grade F · 1 Critical, 1 High

Summary

Naive ERC-4626 vault with the canonical "inflation" / "donation" attack. The first depositor mints 1 share for 1 wei, then donates a large amount directly to the vault via raw transfer. The vault's totalAssets() ballons; the next depositor's reasonable-sized deposit rounds to zero shares — the vault keeps the asset, the user gets nothing.

This pattern caused multi-protocol losses across 2023. OpenZeppelin v4.9 added the canonical defense (virtual shares / virtual assets); this contract uses none of it.

Findings

IDSevTitle
INFL-001CriticalNaive convertToShares math, no virtual shares, no dead-shares, no minimum first deposit
INFL-002HightotalAssets() = balanceOf(this) — donation manipulable
INFL-003HighRound-direction reversed in withdraw (favors withdrawer, not vault)

INFL-001 — Inflation attack (Critical)


shares = assets * totalSupply / totalAssets();   // rounds down to 0 if totalAssets >> assets * totalSupply

Attack:

  1. Attacker deposit(1) → mints 1 share, owns the vault.
  2. Attacker asset.transfer(address(vault), 1e30) → totalAssets is now huge, but totalSupply is still 1.
  3. Victim deposit(1e18)shares = 1e18 * 1 / 1e30 = 0 → victim loses 1 ETH for 0 shares.
  4. Attacker withdraw(1) → claims totalAssets * 1 / 1 = 1e30 + 1 (their 1 wei + their donation + the victim's deposit).
  5. Defense (use OZ ERC4626 v4.9+):

    
    function _decimalsOffset() internal pure override returns (uint8) {
        return 6;   // virtual shares = 10^6 — donation impact divided by 10^6
    }
    

    INFL-002 — totalAssets() = balanceOf(this) (High)

    
    function totalAssets() public view returns (uint256) {
        return asset.balanceOf(address(this));
    }
    

    Anyone can pump totalAssets() via raw transfer. Track internally via deposit/withdraw events.

    INFL-003 — Round direction reversed in withdraw (High)

    
    assets = shares * totalAssets() / totalSupply;   // rounds DOWN
    

    ERC-4626 spec: withdraw should round UP on shares to favor the vault (i.e. burn slightly more shares than mathematically necessary). This implementation rounds DOWN, which slowly drains rounding-residue value to withdrawers.

    Recommendation

    Replace with OpenZeppelin's ERC4626Upgradeable ≥ v4.9.0:

    
    contract MyVault is ERC4626Upgradeable {
        function _decimalsOffset() internal pure override returns (uint8) { return 6; }
    }
    

    Or — if you need a dependency-free implementation — apply Solady's ERC4626 which uses dead-shares + minimum-first-deposit.

    Skill detail: erc4626-inflation, yield-aggregator-specialist.

    Generated by Rugproof — https://omermaksutii.github.io/RugProof