Ensure the security of your smart contracts

Overview of the Inflation Attack

Author: Konstantin Nekrasov
Security researcher at MixBytes
Intro
An inflation attack is a widespread problem that targets ERC-4626 tokenized vault standard and has largely gone unnoticed until recently. This attack allows malicious actors to steal the first deposits into vulnerable pools, potentially resulting in significant losses for unsuspecting investors. This vulnerability was highlighted in OpenZeppelin ERC4626 audit.

Many projects use the ERC-4626 standard, as seen at erc4626.info, and there were transactions worth millions of dollars in the wild that could have been exploited (but luckily were not). Therefore, we decided to write an article that describes the attack and how to protect against it.
Definition
In the early stages, any exchange pool that utilizes the 'mint shares' function in exchange for underlying assets can be susceptible to an inflation attack, resulting in investors losing some or all of their funds to either the pool or a hacker.

The vulnerability arises from a rounding issue in the 'mint shares' function, as the following equation shows:

sharesAmount = totalShares * assetAmount / asset.balanceOf(address(this))
Hackers can manipulate the denominator, causing a victim to receive either zero or one shares of the vault.
Examples
Let's have a look at three examples of this attack.
Example 1. Rounding shares to zero
The example based on the OpenZeppelin ERC4626 implementation:

abstract contract ERC4626 is ERC20 {
    IERC20 asset;

    constructor(IERC20 asset_) {
        asset = asset_;
    }

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

    function convertToShares(uint256 assets) public view returns (uint256) {
        if (totalAssets() == 0) {
            return assets;
        }
        return totalSupply() * assets / totalAssets();
    }

    function convertToAssets(uint256 shares) public view returns (uint256) {
        return totalAssets() * shares / totalSupply();
    }

    function deposit(uint256 assets) public {
        asset.transferFrom(msg.sender, address(this), assets);
        _mint(msg.sender, convertToShares(assets));
    }

    function burn(uint256 shares) public {
        _burn(msg.sender, shares);
        asset.transfer(msg.sender, convertToAssets(shares));
    }
}
Attack scenario:
  1. A hacker back-runs the transaction of an ERC4626 pool creation.
  2. The hacker mints for themself one share: deposit(1). Thus, totalAsset()==1, totalSupply()==1.
  3. The hacker front-runs the deposit of the victim who wants to deposit 20,000 USDT (20,000.000000).
  4. The hacker inflates the denominator right in front of the victim: asset.transfer(20_000e6). Now totalAsset()==20_000e6 + 1, totalSupply()==1.
  5. Next, the victim's tx takes place. The victim gets 1 * 20_000e6 / (20_000e6 + 1) == 0 shares. The victim gets zero shares.
  6. The hacker burns their share and gets all the money.

How to fix it?
One option is to restrict the minting of zero shares, but this alone doesn't fully address the vulnerability, as demonstrated in the second example.
Example 2. Rounding to one share
Let's assume that we added the next condition to the deposit() function in the example above:

require(convertToShares(assets) != 0);
Then the attack becomes more complicated:
  1. The hacker back-runs a transaction of an ERC4626 pool creation.
  2. The hacker mints for themself one share: deposit(1). Thus, totalAsset()==1, totalSupply()==1.
  3. The hacker front-runs the deposit of the victim who wants to deposit 20,000 USDT (20,000.000000).
  4. The hacker inflates the denominator right in front of the victim: asset.transfer(10_000e6). Now totalAsset()==10_000e6 + 1, totalSupply()==1.
  5. Next, the victim's tx takes place. The victim gets 1 * 20_000e6 / (10_000e6 + 1) == 1 shares. The victim gets only one share, which is the same amount as the hacker has.
  6. The hacker burns their share and gets half of the pool, which is approximately 30_000e6 / 2 == 15_000e6, so their profit is +5,000 (25% of the victim's deposit).

How to fix it? There are different approaches. For example, you can mint "dead shares" on the fisrt deposit.
Example 3. Griefing and dead shares
Let's suppose that we take the additional step of implementing the 'dead shares' technique used by UniswapV2 to protect a pool's deposit() function:

uint constant NUMBER_OF_DEAD_SHARES = 1000;

function deposit(uint256 assets) public {
    asset.transferFrom(msg.sender, address(this), assets);
    uint shares = convertToShares(assets);
    
    if (totalShares() == 0) {
        _mint(address(0), NUMBER_OF_DEAD_SHARES);
        shares -= NUMBER_OF_DEAD_SHARES;
    }
    
    _mint(msg.sender, shares);
}
The complexity of the attack has tripled, and although the hacker can no longer steal funds, there is still a griefing opportunity available:

  1. The hacker back-runs a transaction of an ERC4626 pool creation.
  2. The hacker mints 1,000 shares: deposit(1000). Thus, totalAsset()==1000, totalSupply()==1000. Note that the balanceOf(hacker) == 0 and balanceOf(address(0)) == 1000 in this example.
  3. The hacker front-runs the deposit of the victim who wants to deposit 20,000 USDT (20,000.000000).
  4. The hacker inflates the denominator right in front of the victim: asset.transfer(20_000_000e6). Now totalAsset() == 20_000_000e6 + 1000, totalSupply() == 1000.
  5. Next, the victim's tx takes place. The victim gets 1000 * 20_000e6 / (20_000_000e6 + 1000) == 0 shares. The victim gets zero shares, losing their deposit to the pool.
  6. Thus, the hacker burns any deposit of the victim, but spends a thousand times more money to do so.
Conclusion
In this article, we have analyzed several examples of vulnerable code that can lead to an Inflation Attack on a ERC-4626 vault. We discussed a few approaches of how to protect against the attack and their flaws. A more detailed list of different approaches, along with their pros and cons, can be found in the OpenZeppelin github issue. It is crucial for auditors and developers to be aware of how this attack works to safeguard DeFi vaults and pools at an early stage.
Links
Other posts