On December 8, I alerted the Compound team about a bug in its smart contract code that would allow a malicious borrower to steal funds. That same day, the engineering team flipped a switch on their smart contract to disable new borrowing. Then, a few days later, the team deployed a patch that was reviewed by its initial auditors and allowed borrowing to resume. With the protocol operating smoothly since the fix, I thought I should share a few thoughts on the issue.
How I Found the Bug
It all started with this Twitter thread. This is an easy one, I thought. Borrowers pay interest in the asset they borrow (Dai in this example, not Ether). The only time a borrower would pay interest with their collateral is if the position gets liquidated.
This is correct, but the responses in the thread were all over the place. I had taken a quick look through Compound’s smart contract when it first launched, but this made me question its mechanics enough to take a second look.
Compound incentivizes third-party agents, called liquidators, to ensure that the value of assets a borrower has in the money market is at least a certain percentage (currently 150%) of the value they owe from borrowing. If a borrower is below this threshold, a liquidator can take some of the borrower’s collateral at a discounted price and pay down enough of the loan so that the borrower’s position is sufficiently collateralized again.
The accounting for this process is straightforward. Subtract from the borrower’s collateral balance and add to the liquidator’s collateral balance.
However, Compound’s smart contract uses an interesting design pattern. In an effort to optimize for gas costs and error handling, the contract always reads from storage first, then does intermediate calculations using local variables, and then writes the resulting values to storage at the very end.
The actual smart contract code is below (with my comments and emphasis).
// Get the borrower's collateral balance
Balance storage borrowBalance_TargeUnderwaterAsset = borrowBalances[targetAccount][assetBorrow];// Get the liquidator's collateral balance
Balance storage supplyBalance_LiquidatorCollateralAsset = supplyBalances[localResults.liquidator][assetCollateral];// Subtract from the borrower's collateral
(err, localResults.updatedSupplyBalance_TargetCollateralAsset) = sub(localResults.currentSupplyBalance_TargetCollateralAsset, localResults.seizeSupplyAmount_TargetCollateralAsset);// Add to the liquidator's collateral
(err, localResults.updatedSupplyBalance_LiquidatorCollateralAsset) = add(localResults.currentSupplyBalance_LiquidatorCollateralAsset, localResults.seizeSupplyAmount_TargetCollateralAsset);// Save the borrower's new collateral balance
supplyBalance_TargetCollateralAsset.principal = localResults.updatedSupplyBalance_TargetCollateralAsset;// Save the liquidator's new collateral balance
supplyBalance_LiquidatorCollateralAsset.principal = localResults.updatedSupplyBalance_LiquidatorCollateralAsset;
Do you see the error?
This code will work except in the case that the liquidator is also the borrower. In that case, this code effectively ignores the subtraction and saves a collateral balance that is the original amount plus the seized amount. As a numerical example, if the borrower/liquidator initially had 100 in collateral and 50 was subtracted/added during the liquidation process, that account should end up with 100 (=100–50+50) after liquidation, but would actually have 150!
The bug would allow a malicious borrower to liquidate their own position and take additional collateral from the protocol.
At first glance, the issue appears to be mitigated by the fact that there is a competitive marketplace for liquidations. Liquidation opportunities are generally only worth a few dollars before they are taken by an arbitrageur.
However, a malicious miner could have actually done some serious damage by waiting to mine a transaction that updates the price oracle. With this, a miner could take out a large position, execute the price change, liquidate his own position to exploit the bug, and exit the money market within the scope of a single block.
The Compound team deployed a workaround by requiring that all liquidations be funneled through a separate Liquidator contract. The new interest rate model detects whether it is being called in the context of a liquidation and will revert if the liquidation does not originate at the Liquidator contract.
Compound deserves credit for building an ambitious smart contract protocol. For example, Compound is the only lending protocol in which:
- Interest rates adjust algorithmically based on demand to supply and borrow assets
- Multiple assets can be used as collateral for a single position
- A borrower’s collateral earns interest
In response to the confusion on Twitter and many in the Ethereum community about how Compound works, I plan to write more detailed blog posts on all of the major smart contract lending protocols (Compound, Dharma, dYdX, Maker, and our own forthcoming protocol at Marble).
As for the bug, there are some good results from this experience:
- No funds were lost.
- The Compound team was able to mobilize and deploy a fix within a few hours of the initial bug report on a Saturday. This was seriously impressive.
- The Compound team awarded a bug bounty. This will encourage future responsible disclosures.
And for the remaining lessons learned:
- Public bug bounties can be helpful. Compound’s smart contracts were audited by two security firms, including one of the most reputable firms in the industry. However, a public bug bounty can get more eyes on the protocol and potentially prevent an issue like this before launch.
- Teams should use established smart contract best practices. Compound’s money market would be a lot easier to audit if it simply reverted instead of trying to fail gracefully everywhere.
- Users should know that there is some degree of centralization in many smart contract protocols like Compound. The team was able to deploy a fix so quickly because it has special controls on the smart contract. The promise of this technology is to open up new markets without requiring trust in any institution.
- Open financial protocols like Compound’s money market are exciting, but early and experimental. There may still be bugs that result in losses.