Inflation attack: from idea to code
In simple terms, an inflation attack is a malicious attacker front-running the vault’s first depositor, manipulating solidity’s rounding down and taking all the shares. But let’s start from the basics.
What is a Vault and how does it work?
When talking about the vault many refer to OpenZepplin’s ERC4626 standard. Their contract is a simple vault with easy ways for developers to reconfigure the contract’s behaviour. But, a vault is not strictly ERC4626 standard. It can be any contract that is used to store users’ funds for whatever reason, e.g. vault funds are used as liquidity for perps or lending protocols, they can acquire yield, etc.
When users deposit into the protocol, they get shares in return. For example, let’s imagine that we have a Shieldify vault. Users deposit <code-word>DAI<code-word> and we give them some amount of vault tokens, let’s call it <code-word>SHIELD<code-word>.
Initially, <code-word>DAI:SHIELD<code-word> ratio equals <code-word>1:1<code-word>. With time, deposited tokens accrued yield and ratio moved from <code-word>1:1<code-word> to, for example, <code-word>1.5 DAI:1 SHIELD<code-word>, which means that users who deposited at a ratio of <code-word>1:1<code-word> can now withdraw 1.5 times more: they sent 10,000 DAI and received 10,000 SHIELD, now they will redeem 15,000 DAI. Great, now we understand how vaults work, but where does the inflation attack occur?
Inflation Attack: Theory
Deposit
Before diving into the attack scenario, we have to understand what is the most basic formula to calculate the shares:
<code-word>amountOfDeposit<code-word> — how much a user is going to deposit.
<code-word>totalSupplyOfVault<code-word> — total supply of vault tokens
<code-word>balanceOfDeposit<code-word> — vault’s balance of deposit token.
The first problem that comes to mind is how we will mint the tokens for the first depositor if <code-word>totalSupplyOfVaul<code-word>t and <code-word>balanceOfDeposit<code-word> are 0.
The answer is quite easy, many protocols mint the amount of vault tokens equal to the user’s deposit.
For example, let’s imagine our Shieldify vault has 150,000 DAI and 100,000 SHIELD tokens. Some user wants to deposit another 50,000 DAI, so how much would they receive?
Our depositor will receive 33,333 SHIELD tokens (in fact, it would be slightly above, around 33,333.333 tokens, but that’s not so important here).
After the deposit, the balance is the following:
<code-word>totalSupplyOfVault<code-word> — 133,333 SHIELD.
<code-word>balanceOfDeposit<code-word> — 200,000 DAI.
Now, let’s see how withdraw logic works.
Withdraw
The logic is similar, but the formula’s quite different:
<code-word>amountOfWithdraw<code-word> — the amount of vault tokens a user wants to return.
<code-word>balanceOfDeposit<code-word> — the balance of the deposit token (DAI in our example).
<code-word>totalSupplyOfVault<code-word> — total supply of vault tokens.
Let’s imagine, our depositor from the first example wants to withdraw his DAI and returns his 33,333 SHIELD:
The user gets back his 50,000 DAI (in fact, it will be a bit lower, around 49,999.625, but that’s, again, not important here).
And now it’s time for the actual attack scenario.
Inflation Attack: Scenario
We’ve just deployed our Shieldify vault and waiting for the depositors to come in. Bob, as a careful DeFi enthusiast, is going to deposit only a small amount of portfolio in the newly created vault — 1,000 DAI. Alice sees his transaction in the mempool and decides to steal Bob’s token exploiting the inflation attack.
Firstly, she front-runs Bob’s 1,000 DAI deposit with only 1 wei of DAI, which is 1e-18 of DAI. Since she’s the first depositor, she gets minted the amount of shares exactly equal to her deposit. In this case, it will be 1 wei of SHIELD. Therefore, the balance is the following:<code-word>balanceOfDeposit<code-word> = 1 wei of DAI.
<code-word>totalSupplyOfVault<code-word> = 1 wei of SHIELD.
Secondly, she front-runs Bob again! But this time, she directly <code-word>transfers<code-word> 1,000 DAI into the vault (note: not deposits, she uses DAI’s transfer function). Therefore, the vault’s balance is the following:<code-word>balanceOfDeposit<code-word> = 1,000 + 1 wei of DAI.
<code-word>totalSupplyOfVault<code-word> = 1 wei of SHIELD (since the deposit function wasn’t triggered).
Finally, Bob’s deposit comes through, let’s calculate how many shares he will receive:
As we all know, there are no floating numbers in solidity, therefore, the division <code-word>1,000 DAI / 1,000+1 wei DAI<code-word> will result in 0, which means Bob didn’t get any shares minted.
There’s only 1 wei of SHIELD tokens which was minted to Alice in the very first step, let’s see how much she can withdraw after Bob’s deposit:
As we can see Alice may withdraw all the funds from the vault, essentially, stealing what Bob deposited.
In the next section, we’ll dive into the vault code vulnerable to the inflation attack from Code4rena.
Inflation Attack: Code
In this section, we will deep dive into the third high-severity finding in Code4rena’s KelpDAO contest.
In simple terms, wardens had to audit a vault, node delegators (they’re used to invest in strategies), rsETH token (vault token) and oracle to calculate the price of rsETH. Let’s look at how the attacker could exploit an inflation attack in that protocol.
Firstly, here’s the <code-word>depositAsset<code-word> function inside the vault contract:
The interesting thing here is L141 where we call <code-word>_mintRsETH<code-word>:
As you can see it takes the amount that the user deposited and calculates how much rsETH should be minted for that amount of deposit. Let’s go into <code-word>getRsETHAmountToMint<code-word>:
At first glance, it looks different from what we showed in the theory section. To understand how this equation calculates the amount of rsETH, we have to dive into both <code-word>getAssetPrice<code-word> and <code-word>getRSETHPrice<code-word>:
<code-word>getAssetPrice<code-word> function just returns the Asset/ETH exchange rate.
But the interesting point is inside <code-word>getRSETHPrice<code-word>:
Firstly, it calculates how much ETH is in the vault in total by converting each asset into ETH. The important thing to note here is how we get the amount of assets the vault has. When we go into <code-word>getTotalAssetDeposits<code-word>, it directs us to <code-word>getAssetDistributionData<code-word> where it uses balanceOf to get the amount of deposit assets inside the vault.
Secondly, <code-word>getRSETHPrice<code-word> divides it by the rsETH’s total supply. And as you can see if rsETH total supply is 0, then the price returned is 1 ether. But, where are the inflation attacks and rounding down precision loss exactly? Let’s look at an example.
We’ve just deployed WETH/rsETH vault and Bob wants to deposit 10 WETH. The balances are the following:
<code-word>balanceOfWeth<code-word> = 0.
<code-word>totalSupplyRsETH<code-word> = 0.
Alice sees Bob’s transaction and front runs it with depositing 1 wei of WETH. Since there’s no rsETH minted, it returns the price of 1 ether and we have the following equation:
Alice receives 1 wei of rsETH. Now she front-runs Bob again and directly transfers 10 WETH into the contract.
And when Bob’s transaction goes through, the <code-word>balanceOfWeth<code-word> equals 10+1 wei WETH and <code-word>totalSupplyRsETH<code-word> equals 1 wei. Let’s look at the result of his deposit:
And due to rounding down in Solidity, Bob got minted 0 shares and now Alice can withdraw 20 WETH + 1 wei. The next question we’re going to answer is how to save users and protocols from inflation attacks.
Inflation Attack: Mitigations
One of the most popular may be the following:
1. Using the epoch system — users don’t get vault tokens as soon as they deposit, but they have to wait for another epoch/round to start when they can receive their shares.
2. Bootstrapping the vault — minting several shares to address(0) or any other address (team, vault’s address, other contract address, etc.), therefore, there is no first depositor to front-run.
3. Adding decimals to vault tokens — solution from OpenZeppelin, which still allows the inflation attack to take place, but makes it exponentially more expensive with every decimal added.
4. Change Calculation Logic — don’t use <code-word>balanceOf<code-word> for deposit tokens when calculating the vault token exchange rate.
These are only several recommendations to prevent the inflation attack or make it so expensive that attackers won’t exploit it. However, implementing them still cannot save the protocol 100%, so if you’re planning to deploy a system with a vault contract, hit the ‘Contact’ button at the top-right of your screen.
Resources: