$1,000,000 in security audit grants are live now, Apply here →

← Resource Hub

ERC-4626 Inflation Attack: 3 Red Flags and 2 Fixes

Updated 11 min read

Quick answer. An ERC4626 inflation attack front-runs a vault's first deposit. The attacker mints one wei of shares and donates assets to inflate the share price, so the victim's shares round to zero and the attacker redeems the victim's deposit.

The ERC4626 inflation attack is the most common vulnerability in vault systems, and in accounting systems in general. It also goes by first depositor attack or donation attack.

This guide covers how vaults price their shares, the three red flags that signal an inflation attack, and a full walk-through of the exploit.

It then traces the bug through two real codebases, which shows it is not limited to canonical ERC-4626 vaults. It closes with two fixes and their trade-offs.

How Vaults Work

In DeFi terms, a vault is a smart contract, or a set of them, where users deposit funds that the system's logic then manages.

In return, each depositor gets a receipt token. That receipt token is what this attack targets.

Say a vault accepts wrapped ether (WETH). You deposit 1 WETH and receive vault tokens (call them V) at some exchange rate, here 1 WETH per V.

The vault token is an IOU. You can bring it back later and redeem it for WETH at whatever the exchange rate is then.

That rate can change. Maybe it was 1 WETH per V at block 100, and the vault's WETH then earned yield in some strategy.

By block 1,050 the rate is 1.2 WETH per V, so your 1 V now redeems for 1.2 WETH.

This lets many depositors pool funds and put them to work, lending them out or deploying them into other protocols.

Each depositor's share of the yield matches how much of the underlying asset they put in.

The Three Red Flags

Inflation attacks rely on the exchange rate between the vault token and the underlying asset. Three things around that rate matter.

When all three are present, an inflation attack is very likely.

1. balanceOf Used in the Accounting

Vaults often price shares from their own token balance. Divide the vault's WETH balanceOf by the vault token supply, and you have the exchange rate.

It is intuitive, and it is a reasonable design. But anyone can send tokens straight to the contract, so it is manipulable.

2. Rounding Down on the Share Calculation

The second red flag is precision loss, or rounding down, when the vault computes how many shares to mint for a deposit.

Suppose the vault holds 100 WETH and 50 V exist, so the rate is 2 WETH per V. You deposit 50 WETH:

// shares = amount * totalSupply / balanceOf
// 50 WETH * 50 V / 100 WETH = 25 V
uint256 shares = (amount * totalSupply()) / asset.balanceOf(address(this));

When formulas get complex, write out the units of each term. Here it is WETH times V divided by WETH.

The WETH cancels and you get V, so the formula at least returns the right units.

In magnitude, it values the deposit at the ratio of vault token supply to vault balance. That ratio is the exchange rate.

The precision loss sits in the division. Solidity has no decimals, so 10,000 divided by 10,001 is zero.

For more on this, see rounding errors in Solidity.

3. First Depositor Logic

The third red flag is the logic for the first depositor. Strictly, it covers any depositor before assets and shares pass some threshold, but usually it means the very first.

When nobody has deposited, the balance is zero. Most systems then assume a 1:1 exchange rate: deposit 10 WETH first and you get 10 V.

Check whether the protocol does anything special here, such as making a first deposit on users' behalf.

If it does nothing beyond the 1:1 rule, and the first two flags are present, you very likely have an inflation attack.

How the Attack Works

The attack abuses the share formula, and specifically Solidity's rounding down in division.

The victim, trying to be the first depositor, loses the vault tokens they should have been minted. The assets behind those lost shares go to the attacker.

Bob submits a transaction to deposit 10 WETH. He expects to be first and to receive 10 V at the initial 1:1 rate.

Alice sees Bob's transaction in the mempool and front-runs him.

For the mechanics, see front-running in smart contracts. Alice takes three steps:

  1. She deposits a single wei of WETH and receives one wei of V. Total supply is now one wei.
  2. She transfers 10 WETH straight to the vault without calling deposit. She gets no shares, but the vault's balanceOf is now 10 WETH plus one wei.
  3. She waits for Bob's deposit to execute.

Bob's shares come out as 10 WETH times one wei of supply, divided by 10 WETH plus one wei. In Solidity that truncates to zero.

Bob has deposited 10 WETH and received nothing. Alice holds every vault token in existence: one wei.

Redeeming uses the inverse formula: shares times balance divided by total supply. The units work out to WETH.

Alice's one wei of shares times a balance of about 20 WETH, divided by a supply of one wei, returns about 20 WETH.

She put in just over 10 WETH, so she roughly doubled her money, all of it from Bob. That is the crux of the inflation attack.

Example: Beefy Vault V5

An old Beefy Finance vault, Beefy Vault V5, shows the bug in its deposit function. Run the three checks.

Balance in the accounting. Shares are the amount times the total supply, divided by _pool.

_pool comes from a balance() function: the token's balanceOf this contract, plus funds held in a yield strategy.

The balanceOf alone is enough, since anyone can inflate it by sending tokens to the contract. Check.

Rounding down. The contract is old enough to use SafeMath, but div rounds down exactly like plain division. Check.

First depositor logic. If total supply is zero, you get shares equal to the amount you specify.

Nothing else handles that case, and there is no initial protocol deposit. Check.

Here is the pattern, simplified and written in ^0.8 arithmetic instead of SafeMath:

function deposit(uint256 amount) external {
    uint256 pool = balance(); // token.balanceOf(address(this)) + strategy funds
    token.safeTransferFrom(msg.sender, address(this), amount);
    uint256 shares;
    if (totalSupply() == 0) {
        shares = amount; // first depositor: 1:1
    } else {
        shares = (amount * totalSupply()) / pool; // rounds down
    }
    _mint(msg.sender, shares);
}

The attacker mints a single wei because total supply must be as small as possible.

At even one or two orders of magnitude more, they must send far more tokens to grow the denominator enough to round the victim down.

These few lines are the core of ERC-4626, the tokenized vault standard. It gives depositors slices of a pot of assets that earns yield.

The vault is itself an ERC-20 token, and it mints shares to the depositor from this formula.

Beefy Vault V5 is not fully compliant with the standard. It has no mint or redeem functions.

It also predates OpenZeppelin's ERC-4626 contract by a year and a half to two years. It is an early form of the pattern.

Many vaults look like this: an if that mints 1:1 when supply is zero, and an else that converts using supply against balance.

Systems that look nothing like an ERC-4626 vault can still be vulnerable.

Case Study: Kelp

Kelp was a recent Code4rena contest at the time of recording (late 2023). It shows the inflation attack keeps coming up in real protocols and real contests.

The relevant contract is LRTDepositPool. Users deposit liquid staked ether tokens, such as Rocket Pool ETH or Lido staked ETH, and receive rsETH.

Kelp does not call rsETH a vault token, but that is what it is: an IOU for withdrawing from the pool later.

So, as always, go to the logic that decides how many shares to mint.

The Mint Formula

depositAsset calls _mintRsETH, which calls getRsETHAmountToMint.

That function multiplies the deposit amount by the asset's oracle price in ether, then divides by the rsETH price, also in ether.

Write out the units. The amount is in staked ETH, the asset price is ETH per staked ETH, and the rsETH price is ETH per rsETH.

Dividing by ETH per rsETH means multiplying by rsETH per ETH. The ETH and staked ETH terms cancel, leaving rsETH, so the formula is sound.

Now simplify. The amount times the asset price is just the deposit's value in ether.

The rsETH price is the ETH in the pool divided by the rsETH supply. Dividing by it means multiplying by supply over ETH.

So the formula becomes ether amount times total supply, divided by total balance. It is the same formula as Beefy's.

Checking the Three Flags

First depositor logic. In the LRTOracle contract, getRSETHPrice returns one ether when the rsETH supply is zero.

That is a 1:1 rate, with no other handling for the first deposit and no protocol-made first deposit. Check.

Balance in the accounting. Otherwise, getRSETHPrice loops over the supported assets and returns the total ETH in the pool divided by the rsETH supply.

Each asset's amount comes from getTotalAssetDeposits, which leads to getAssetDistributionData.

There is the smoking gun: a balanceOf for the asset lying in the deposit pool.

It feeds the total, the total feeds the price, and the price feeds the mint amount. Check.

Rounding down. getRsETHAmountToMint divides by the rsETH price.

Transfer a pile of staked ETH straight to the pool, and the ETH in the pool rises while the rsETH supply stays the same.

The denominator grows, the numerator does not, and the division loses precision. Check.

This is not a canonical ERC-4626 vault, and it does not claim to be one.

It is still a vault system that uses balanceOf, rounds down and has 1:1 first depositor logic. So it is vulnerable.

The Attack on Kelp

  1. Bob submits a deposit of 10 staked ETH.
  2. Alice front-runs him and deposits one wei. The rsETH supply is zero, so the price is one ether and she is minted one wei of rsETH.
  3. Alice transfers 10 staked ETH directly to LRTDepositPool, outside the deposit flow.
  4. Bob's deposit executes. The pool holds about 10 ETH plus one wei against one wei of rsETH, so the rsETH price is about 10 ether plus one wei.
  5. Bob's 10 ETH of value divided by that price truncates to zero. He gets no rsETH, but his 10 staked ETH still enter the pool.

The same scenario plays out: Alice makes off with Bob's deposit.

Fix 1: An Initial Deposit

The protocol can make the first deposit itself, for example by depositing a trivial amount of tokens in the constructor.

The crux of the issue is that the attacker sets the initial exchange rate and makes the total supply a single wei.

That tiny supply is what lets a deposit round to zero, in Beefy and in Kelp alike.

With a protocol deposit, the supply can never be one wei or a handful of wei.

Against even one full vault token (18 decimals), rounding a deposit to zero takes an unrealistic donation. The attack stops being economically viable.

Fix 2: OpenZeppelin's Virtual Offset

The second fix is the more interesting one. OpenZeppelin's ERC-4626 contract makes the attack unprofitable, then adds a knob to make it ever more costly.

Using it is probably the best way to avoid the inflation attack.

The change sits in convertToShares and convertToAssets, exactly the logic examined above. The formula gains a virtual offset:

// simplified from OpenZeppelin's ERC4626
shares = assets * (totalSupply() + 10 ** decimalsOffset) / (totalAssets() + 1);
assets = shares * (totalAssets() + 1) / (totalSupply() + 10 ** decimalsOffset);

By default the offset is zero, so each formula adds one to the supply and one to the assets.

Why the Attack Stops Paying

Rerun the attack. Alice deposits one wei, gets one wei of shares and donates 10 WETH.

Bob's 10 WETH times two, divided by 10 WETH plus two wei, now rounds to one share instead of zero.

Alice and Bob each hold one share, and the vault holds about 20 WETH. Alice can no longer walk off with Bob's deposit.

The plus one acts as a virtual share that the vault always keeps. Even when Bob is rounded to zero, Alice is not the sole owner of the shares.

She can never take all the funds. At best she breaks even, and in most cases she loses.

Griefing Still Works

Alice can still round Bob to zero if she donates enough, say 20 WETH.

When she redeems her one share against a supply of two (one real, one virtual), half of the vault's assets stay behind.

She loses part of her own donation, but Bob still lost his deposit. The attack is unprofitable, yet griefing is still an attack vector.

Raising the Offset

Set the decimals offset to two, so the formula adds 100 to the supply.

With Alice's 20 WETH donation, Bob's 10 WETH times 101 (Alice's one share plus 100), divided by about 20 WETH, gives him about 50 shares. He is no longer rounded to zero.

To round Bob to zero now, Alice would have to donate about 1,000 WETH to make him lose 10.

The cost grows exponentially with the offset. The attack goes from unprofitable to extremely costly, even as griefing.

The offset also gives depositors two extra digits of shares. OpenZeppelin treats this as extra precision, not extra value.

The vault's decimals() is the underlying asset's decimals plus the offset. With an 18-decimal asset and an offset of two, one vault token is 1e20 base units.

Inflation Attack Checklist

Find the logic that decides how many shares to mint, then check:

  • Balance. Does the exchange rate depend on balanceOf, anywhere down the call chain?
  • Rounding. Does the share calculation divide and round down?
  • First depositor. Is there a 1:1 rate when supply is zero, with no protocol first deposit?
  • Units. Rewrite custom price formulas in units. Many reduce to amount times supply divided by balance.
  • Fixes. Is there an initial protocol deposit or OpenZeppelin's virtual offset? Is the offset high enough to make griefing costly?

An inflation attack is one kind of stepwise jump. See DeFi reward accounting vulnerabilities for the others.

For prices derived from onchain balances more broadly, see oracle manipulation attacks.

How Guardian Helps

Guardian's smart contract audits build invariant suites around accounting, rounding, solvency and edge states.

Senior researchers then attack the protocol logic with exploit-driven review. See smart contract audits or Vanguard.

Frequently Asked Questions

What is an ERC-4626 inflation attack?

A first depositor attack on a vault. The attacker front-runs the first deposit, mints one wei of shares, then sends assets straight to the vault. The victim's shares round to zero, and the attacker redeems the victim's deposit.

What are the red flags for an inflation attack?

Three together: the exchange rate depends on balanceOf, the share calculation rounds down, and the first depositor gets a 1:1 rate with no protocol first deposit. With all three, an inflation attack is very likely.

Does the attack only affect ERC-4626 vaults?

No. Any system that mints receipt tokens against a pool of assets can carry it. Kelp's rsETH price reduced to the same amount times supply over balance formula, built on balanceOf.

How does OpenZeppelin prevent the inflation attack?

Its ERC-4626 adds a virtual offset to share conversions. With an offset of zero the attack is already unprofitable, and a larger decimals offset makes even griefing extremely costly.

Sources

Get your vault accounting reviewed

Guardian tests share pricing, rounding and first depositor paths before an attacker does.