Community Workshop: Riley Holterhus

Intro

This workshop covers ERC4626 inflation attack.

Video:

Slides:

ERC4626 Inflation Attack

Description

This is the inflation attack I mentioned.

The contract implements basic ERC4626 operations. Users can deposit WETH to get vault shares, and they can burn vault shares to get WETH back, plus interest.

The bug is in the deposit() function:

function deposit(uint256 assets, address receiver) external {
    uint256 sharesToMint;
    if (totalSupply == 0) {
        sharesToMint = assets;
    } else {
        
    }
    require(sharesToMint != uint256(0));
    
    depositToken.transferFrom(msg.sender, address(this), assets);

    _mint(receiver, sharesToMint);
}

The sharesToMint math can go wrong. Recall that integer division in Solidity rounds down. This rounding issue leads to inflation attack against the first victim depositor.

PoC

Here is a table summarizing the attacking steps:

Basically, attacker deposits 1 wei to mint 1 share in a newly-created vault.

Then, the attacker transfers 100 WETH directly to the contract without using the deposit() function. This step is done by transfer() or transferFrom(). The attacker is losing money here to inflate the vault. Now the totalSupply is still 1 share but balanceOf becomes a huge number 1 + 100 * 10**18.

When the first victim depositor deposits, say, 200 WETH, the computation for sharesToMint goes wrong. The formula is:

(assets * totalSupply) / depositToken.balanceOf(address(this))

Substitute in:

sharesToMint = ((200 * 10**18) * (1)) / (1 + 100 * 10**18)
                     |            |              |
                assets: 200 WETH  |          balanceOf
                             totalSupply

sharesToMint = floor(1.9999...) <- Integer division in Solidity rounds down
sharesToMint = 1           <- This is really bad: 1.99 shares become 1 share

We see the victim lost 0.9999... share in this deposit. Now attacker owns 1 share and victim owns 1 share, so they evenly own all the money in the vault.

In the end, attacker withdraws 1 share. How much can the attacker profit:

Attacker's cost = 100 WETH + 1 wei, let's just say the cost is 100 WETH.
The vault balance is 300 WETH + 1 wei, let's say the balance is 300 WETH.
There are 2 shares, so 1 share = 150 WETH.
Attacker's profit = 150 WETH - 100 WETH = 50 WETH.

This bug exists in other protocols

How to fix this bug?

The solution mentioned in the workshop is minting some of the initial amount to the zero address. This makes the "rounded down" shares less dominate in the computation.

I would suggest using a "multiplier" just like what is used for computing floating point numbers. Suppose the multiplier is 6, then 1.9999.. shares -> 1999999.999... shares. The "rounded down" part becomes less important.

Last updated