In this blog post, I explain how Hundred Finance lost $7 million due to a simple rounding error.
On April 15, Hundred Finance was drained by an attacker due to a rounding error in its redeemUnderlying
function. However, this rounding error can only be exploited if there is a market with zero liquidity. Unfortunately, Hundred Finance created two WBTC markets, one of which was used by the UI, but the other market was empty with zero liquidity.
This attack is similar to the well-known ERC4626 first-depositor inflation attack.
First of all, why should we care about this attack?
Because Hundred Finance is a fork of Compound V2, and this attacker vector is in the Compound V2 codebase itself, which is one of the most forked codebases in the DeFi space. So to prevent further exploits on Compound V2 forks, every developer and security researcher should be aware of this issue.
Before diving into the issue, let’s understand how Compound V2 works at a high level.
Compound V2
Compound is an over-collateralized lending protocol where lenders can supply liquidity to a market, and borrowers can borrow funds by providing a different type of asset as collateral. Borrowers will pay interest for the funds they borrow, which will be distributed to lenders based on the amount of liquidity they supplied to the market.
For every asset, there will be a market, and shares (cTokens) for that market will be minted when a user supplies assets to that market based on the current exchange rate.
Whenever we supply assets to a market, we get borrowing power to borrow funds from a different market.
deposit WBTC ---> mint shares
redeem WBTC ---> burn shares
borrowing power = shares * exchangeRate
exchangeRate = how much WBTC each share is worth
Root cause
The root cause of the attack is a rounding issue in the redeemUnderlying
function. Whenever a user redeems their underlying tokens (WBTC in this case), the number of shares to burn is calculated as follows:
shares to burn = underlying token amount / exchangeRate
But instead of rounding up, the number of shares to burn is rounded down in redeemUnderlying
function. Due to this, in some cases, one wei fewer number of shares will be burned. At first glance, this doesn’t seem like an issue because, after all, it’s just one wei of share. In a normal market, this one wei of share isn’t even worth one cent. But what if that one wei of share is worth 250 bitcoins? Exactly, that’s what happened in the Hundread Finance attack. The attacker was able to amplify this small rounding error by inflating the exchange rate to 250 bitcoins.
Redemption process
- Calculate the exchangeRate of the market.
- Calculate the sum of assets borrowed by the user in all markets (active loan value).
- Make sure that the user’s remaining collateral value(value after redemption) covers the active loan value.
- If the remaining collateral covers the active loan, allow redemption.
- If the remainging collateral doesn’t cover the active loan value, revert the transaction.
The Attack
- First the attacker get a 500 WBTC flashloan from AAVE.
-
Then attacker uses a small portion of his WBTC to mint hWBTC(shares), before redeeming all but 0.00000002 (2 wei).
State Before After Attacker shares
0 wei 2 wei Attacker collateral
0 wei 1 wei
-
Then the attacker donated 500 WBTC to the protocol by doing a direct transfer to inflate the exchangeRate to 250 WBTC/ 1 Wei hWBTC.
State Before After Attacker shares
2 Wei 2 Wei Attacker collateral
0.00000001 WBTC 500.00000001 WBTC Exchange Rate
0 WBTC / 1 wei hWBTC 250 WBTC / 1 wei hWBTC Loan Value
0 WBTC 0 WBTC
-
Now the attacker borrowed 70 WBTC worth of ETH from ETH market.
State Before After Attacker shares
2 Wei 2 Wei Attacker collateral
500.00000001 WBTC 500.00000001 WBTC Exchange Rate
250 WBTC / 1 Wei hWBTC 250 WBTC / 1 Wei hWBTC Loan Value
0 WBTC 70 WBTC
-
Then the attacker redeemed
499.99999999
WBTC from their collateral using theredeemedUnderlying
function. However, instead of burning 1.99999999 wei of hWBTC, only 1 wei of hWBTC was burned due to a rounding error. At this point, the attacker was able to withdraw almost all of their collateral tokens, despite having an outstanding loan worth 70 WBTC, because the protocol still thinks that their remaining 1 wei of hWBTC is still worth 250 WBTC which covered their active loan.State Before After Attacker shares
2 Wei 1 Wei Attacker collateral
500.00000001 WBTC 2 wei WBTC Exchange Rate
250 WBTC / 1 wei hWBTC 1 wei WBTC / 1 wei hWBTC Loan Value
70 WBTC 70 WBTC
- At this point, the attacker’s borrowing position is fully underwater, and they ran away with 70 WBTC worth of ether.
How compound v2 forks can avoid this issue?
-
If your protocol is already on-chain, ensure that your markets never reach a zero liquidity state by minting a small amount of shares and sending them to the zero address.
-
When listing a new collateral token, first set its collateral factor to zero, then mint some shares, send them to the zero address, then change the collateral factor to the desired value.
Happy Hunting Anon!