Lesson Learned from Bad Debt Related Bugs
In April 2022, the Inverse Finance lending platform was subject to a price oracle manipulation attack which resulted in over $15m of bad debt.
As a result, liquidity for ETH, WBTC and YFI was drained, and it became impossible for depositors of these assets to withdraw.
In an attempt to resume normal operation, Inverse Finance’s team made the decision and decided not to launch the new lending market from scratch. Instead they deployed new ETH, WBTC and YFI markets, and took actions to try and deprecate the affected markets.
In June 2022, per a Twitter follower request, the Risk DAO team started to monitor Inverse Finance’s bad debt. In an attempt to understand the bad debt consequences on current users of Inverse, we realised the deprecation actions of Inverse were flawed, and reported a possible attack vector to the team. The report triggered a sequence of interactions, where the Inverse team was implementing imperfect mitigations, and we were reporting additional flaws.
In this report we describe the sequence of events. In addition to presenting (potentially) interesting technical subtleties, the writeup also shows that changes in lending market code are very nuanced, and that deciding not to re-launch an affected market from scratch is risky.
Background
Inverse Finance is a Compound-compatible lending market. To simplify the technical explanation we assume it allows users to deposit only WBTC into a cWBTCv1 contract and DOLA (a stable coin) into a cDOLA contract. Further, it lets users borrow DOLA against their WBTC deposit.
The attack of April 2022 drained all the available WBTC liquidity in the cWBTCv1 contract.
After the hack, the Inverse team launched another WBTC deposit contract, namely, cWBTCv2. To resume normal operation, it was imperative that the bad debt accrued in cWBTCv1 will not affect the new users in the cWBTCv2 and DOLA markets. In other words, it was important to prevent cWBTCv1 depositors from borrowing more DOLA, as it will come at the expense of the liquidity that is supplied by the new users (it is important to note that cWBTCv1 users had done no malicious actions, and are the victims of the April attack).
In the sequel, we describe a series of four attempts to protect the new users, and the attack vectors for the first three attempts.
Attempt 1 — pause borrowing
The Fix
In order to prevent further borrowing from cWBTCv1 users on one hand, and to prevent forced liquidations of cWBTCv1 users on the other hand, the Inverse team decided to maintain normal collateral factor and oracle price for cWBTCv1 deposits, and to block borrows by returning an error when trying to calculate the borrow power of a user that holds cWBTCv1.
// Check if the collateral is paused, only for borrowing
if (
collateralGuardianPaused[address(asset)] == true &&
borrowAmount > 0 &&
vars.cTokenBalance > 0
) {
return (Error.COLLATERAL_PAUSED, 0, 0);
}
The Attack
While the fix halted further borrows, it kept the theoretical borrowing power of cWBTCv1 users. Further, Compound implementation allows users to transfer their cWBTCv1 holdings, which give rise to the following attack:
- Alice, who has cWBTCv1 deposits, opens a fresh account in Inverse Finance, and we denote the new account as Bob.
- Bob takes a WBTC flashloan, deposits it in cWBTCv2, and borrows DOLA.
- Alice transfers the cWBTCv1 token to Bob.
- Bob withdraws his WBTC from cWBTCv2 and returns the flash loan.
The attack sequence ends with Alice having an empty account, and Bob has Alice’s original holdings, with additional DOLA borrowed. Since Alice and Bob are the same person, the goal of the fix was violated.
Attempt 2 — lower collateral factor
Inverse’s team preferred not to pause the cWBTCv1 transfers, and instead raised a governance proposal (which since then was revoked) to decrease the collateral factor of cWBTCv1 to 50%.
With such a collateral factor, a cWBTCv1 with $1m of (lost) deposits could borrow at most $0.5m of DOLA before getting liquidated. And as the team is committed to eventually return the bad debt, they believe that 50% penalty is not something that users will be willing to pay (or alternatively that is a good deal for the team, who is committed to liquidating the cWBTCv1 deposits).
The Attack
While the 50% collateral factor prevents users from borrowing more than 50% than their deposits, it is not the amount they lose when being liquidated. Hence, after a user gets liquidated, part of his debt was repaid, and he can continue to borrow more.
Eventually, either at a certain point no one will liquidate the user (as the liquidator gets cWBTCv1 in return, which is illiquid), and then the users didn’t lose anything, or liquidators (possibly from Inverse team, who continue to liquidated cWBTCv1 debts) will liquidate the full amount, and the user will end up with 90% of liquid DOLA in return to 100% of his illiquid cWBTCv1 debt.
Hence, the potential loss to the user is 10% and not 50% as originally described in the proposal.
Attempt 3 — pause transfer
After reporting the second attack vector to the team, they decided to pause all ctoken transfers, as an intermediary step, until a more focused solution to freeze only cWBTCv1 transfers is implemented.
At this point we asked for Inverse team approval to report the bug. While writing the report another attack vector popped into our mind.
The Attack
While borrowing and ctoken transfer were paused, the Inverse team never paused further WBTC deposits into the cWBTCv1 contract. Hence, a transfer operation could be done in an implicit manner:
- Alice, who holds cWBTCv1 with an underlying balance of X WBTC, opens a fresh account called Bob.
- Bob takes a flash loan and deposits X WBTC into the cWBTCv1 contract.
- Alice withdraws the X WBTC from cWTBCv1, and transfers them to Bob.
- Bob returns the WBTC flashloan.
We end up with Bob holding all of Alice’s cWBTCv1 tokens, and a transfer operation was effectively executed.
Note that step 3 is possible only because Bob previously deposited WBTC into the cWBTCv1 contract. And the attack is risk free only if Alice’s account is managed by a smart contract. If not, some risk is taken when trying to bundle the 4 operations atomically with a flashbot transaction.
Attempt 4 — pause minting
After reporting the 3rd attack vector to the team, they disabled additional cWBTCv1 deposits by enabling the mint pause guardian
Lesson learned
Reasoning about lending platform attack vectors is hard. Code changes are risky, and if possible, it is better to re-launch the platform from scratch after an attack has happened.
Disclaimer
We were never engaged or paid by the Inverse Finance team to review their contracts. We never did a full review of the entire system, and the contract may still have bugs.
About Risk DAO
Risk DAO is a service DAO focused on providing a new, open source risk assessment framework and associated audits to DeFi lending and borrowing protocols as well as L1 networks.
You can follow us on Twitter here. You can join our Discord here.