What is a share inflation attack?

An ERC-4626 vault mints shares in exchange for deposited assets. In a simple model, the share price is determined by:

  • total assets held by the vault
  • total shares already issued

If the vault starts empty or nearly empty, the first depositor can manipulate the exchange rate by making a tiny deposit, then donating assets directly to the vault without minting shares. Later depositors receive fewer shares than expected because the vault now appears more valuable per share.

Example attack flow

  1. Attacker deposits 1 wei and receives 1 share.
  2. Attacker transfers a large amount of assets directly to the vault contract.
  3. The vault’s asset balance increases, but total shares do not.
  4. A victim deposits assets and receives very few shares due to the inflated asset-to-share ratio.
  5. The attacker withdraws a disproportionate amount of value.

This is not a reentrancy issue or an approval issue. It is an accounting and initialization problem that appears when vault math assumes honest asset inflows.

Why ERC-4626 vaults are vulnerable

ERC-4626 standardizes deposit, mint, withdraw, and redeem, but it does not force a specific anti-manipulation strategy. Vulnerability usually comes from one or more of the following:

  • Empty vault bootstrap: no initial liquidity or seed shares
  • Direct asset transfers: assets can be sent to the vault without calling deposit
  • Rounding down: small deposits may mint zero or near-zero shares
  • Unprotected previews: previewDeposit and convertToShares can be misleading in manipulated states

The core issue is that the vault’s pricing function becomes attacker-controlled when the initial share supply is too small.

A minimal vulnerable pattern

The following simplified vault mints shares based on current assets and supply, but it does not defend against donation-based manipulation:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract VulnerableVault is ERC4626 {
    constructor(IERC20 asset) ERC20("Vault Share", "VSH") ERC4626(asset) {}

    function deposit(uint256 assets, address receiver)
        public
        override
        returns (uint256 shares)
    {
        shares = previewDeposit(assets);
        require(shares > 0, "zero shares");
        asset().transferFrom(msg.sender, address(this), assets);
        _mint(receiver, shares);
    }

    function previewDeposit(uint256 assets)
        public
        view
        override
        returns (uint256)
    {
        uint256 supply = totalSupply();
        uint256 totalAssets_ = totalAssets();

        if (supply == 0) {
            return assets;
        }

        return assets * supply / totalAssets_;
    }
}

At first glance this looks reasonable, but it is still vulnerable. If the vault has one share outstanding and a large amount of donated assets, totalAssets() rises while totalSupply() stays fixed, causing new deposits to mint too few shares.

Defensive design strategies

There is no single fix. Robust vaults usually combine several defenses.

DefensePurposeTradeoff
Seed liquidityPrevents extreme initial exchange ratesRequires an initial capital contribution
Minimum share mintingAvoids zero-share depositsMay reject tiny deposits
Virtual assets and sharesReduces the impact of donation attacksAdds accounting complexity
Locked initial sharesStabilizes early pricingRequires careful governance
Deposit caps during bootstrapLimits exposure while vault is youngSlower early growth

The best choice depends on whether the vault is permissioned, public, or intended for high-volume DeFi integrations.

Use seed liquidity or locked initial shares

The simplest mitigation is to ensure the vault is never truly empty. A trusted initializer can deposit assets and mint shares to a burn address or a locked treasury address. This creates a non-zero supply and makes the initial exchange rate less manipulable.

Practical pattern

  • Mint a small, fixed amount of shares on deployment
  • Lock those shares forever or for a long period
  • Ensure the initial asset amount is meaningful relative to expected deposits

This does not eliminate donation attacks entirely, but it makes them much more expensive.

Why it helps

If the vault starts with a meaningful asset base, an attacker must donate a much larger amount to distort pricing enough to exploit later users. The attack becomes capital-inefficient instead of trivial.

Add virtual assets and virtual shares

A more robust approach is to include “virtual” balances in conversion math. This technique is widely used to smooth the initial exchange rate and reduce the effect of donations.

Instead of computing shares from raw totalAssets() and totalSupply(), the vault adds constants:

  • virtualAssets
  • virtualShares

This ensures the denominator and numerator never collapse to zero and prevents extreme price swings in the early lifecycle.

Example implementation

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract ProtectedVault is ERC4626 {
    uint256 private constant VIRTUAL_ASSETS = 1e6;
    uint256 private constant VIRTUAL_SHARES = 1e6;

    constructor(IERC20 asset) ERC20("Protected Share", "PSH") ERC4626(asset) {}

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

    function convertToShares(uint256 assets)
        public
        view
        override
        returns (uint256)
    {
        uint256 supply = totalSupply();
        uint256 managedAssets = totalAssets();

        return (assets * (supply + VIRTUAL_SHARES)) / (managedAssets + VIRTUAL_ASSETS);
    }

    function convertToAssets(uint256 shares)
        public
        view
        override
        returns (uint256)
    {
        uint256 supply = totalSupply();
        uint256 managedAssets = totalAssets();

        return (shares * (managedAssets + VIRTUAL_ASSETS)) / (supply + VIRTUAL_SHARES);
    }
}

Important notes

  • The constants should be chosen carefully relative to the asset’s decimals and expected deposit sizes.
  • Virtual balances should be consistent across all conversion functions.
  • If you override one conversion function, override the related preview and mint/redeem logic as well, or inherit behavior that uses your conversion functions correctly.

Prevent zero-share deposits

A common failure mode is allowing a deposit that mints zero shares due to rounding. Even if the vault is not directly exploitable, zero-share deposits create a terrible user experience and can be abused to trap funds in edge cases.

Best practice

Reject deposits that would mint fewer than a minimum number of shares:

uint256 shares = previewDeposit(assets);
require(shares >= MIN_SHARES, "deposit too small");

You can also enforce a minimum asset threshold:

require(assets >= MIN_ASSET_DEPOSIT, "deposit too small");

This is especially useful when the vault handles tokens with low decimals or highly volatile exchange rates.

Be careful with direct token transfers

ERC-4626 vaults often assume that totalAssets() reflects assets deposited through the vault interface. But ERC-20 tokens can be transferred directly to the vault contract, bypassing deposit.

That means:

  • asset().balanceOf(address(this)) may include donations
  • totalAssets() may rise without corresponding shares
  • conversion math may be skewed

Mitigation options

  1. Treat direct transfers as donations
  • Accept that they increase vault value for all shareholders
  • Ensure they do not disproportionately affect early pricing
  1. Use internal accounting
  • Track assets managed through deposits and withdrawals
  • Ignore unsolicited transfers in pricing logic
  1. Combine with virtual balances
  • Makes unsolicited transfers less harmful

Internal accounting is the most precise option, but it requires careful reconciliation with strategy profits, losses, and external yield.

Use internal accounting for managed assets

If the vault deploys assets into an external strategy, asset().balanceOf(address(this)) may not equal the true managed balance. In that case, totalAssets() should reflect the sum of:

  • idle assets held by the vault
  • assets deployed in the strategy
  • realized profits or losses

This is also the right place to defend against donation attacks, because pricing should depend on tracked accounting rather than raw token balance alone.

Pattern

  • Maintain an internal managedAssets variable
  • Update it only during authorized deposit, withdrawal, harvest, or loss events
  • Do not let arbitrary transfers directly affect share pricing

This makes the vault’s economics explicit and auditable.

Test the attack path explicitly

Security for ERC-4626 vaults is not complete without adversarial tests. You should simulate:

  • empty vault bootstrap
  • tiny first deposit
  • large donation to the vault
  • victim deposit after donation
  • withdrawal by attacker

Example test scenario

  • Attacker deposits 1 token
  • Attacker donates 1,000,000 tokens directly
  • Victim deposits 100 tokens
  • Verify victim receives a fair share amount or that the deposit is rejected

A good test suite should also cover:

  • deposits near rounding boundaries
  • tokens with 6 decimals and 18 decimals
  • repeated donation and withdrawal cycles
  • preview functions before and after manipulation

Recommended implementation checklist

Before deploying an ERC-4626 vault, verify the following:

  • totalAssets() reflects the intended accounting model
  • convertToShares() and convertToAssets() are resistant to empty-vault distortion
  • zero-share deposits are rejected
  • bootstrap liquidity is seeded or virtual balances are used
  • direct token transfers do not create exploitable pricing shifts
  • preview functions match actual mint/burn behavior
  • tests cover donation-based manipulation

When to use each mitigation

ScenarioRecommended approach
Public vault with open depositsVirtual assets and shares, plus minimum deposit checks
Permissioned vault with known initializerSeed liquidity and locked initial shares
Strategy vault with off-chain accountingInternal accounting plus donation tolerance
Low-value or experimental vaultMinimum deposit thresholds and strict bootstrap controls

In practice, public vaults benefit most from layered defenses. Relying on only one mitigation is usually not enough.

Common mistakes to avoid

1. Assuming balanceOf is authoritative

Raw token balance is not always the same as managed assets.

2. Using naive first-deposit math

Returning assets when totalSupply() == 0 is convenient but often unsafe without bootstrap controls.

3. Ignoring rounding effects

Small deposits can mint zero shares or create disproportionate pricing changes.

4. Overlooking direct transfers

Attackers do not need to call your deposit function to influence vault balance.

5. Failing to test manipulated states

A vault that works in normal conditions may fail badly after a donation or a tiny bootstrap deposit.

Conclusion

ERC-4626 share inflation attacks are a pricing and initialization problem, not a low-level call problem. They occur when an attacker can distort the asset-to-share ratio before honest users deposit. The safest vaults combine bootstrap liquidity, virtual balances, minimum deposit checks, and explicit accounting for managed assets.

If you are building a public vault, assume that direct transfers, tiny deposits, and manipulated exchange rates will happen. Design your conversion logic so that early-state economics remain stable even under adversarial conditions.

Learn more with useful resources