Analysis of a Bug in the Compound Protocol

7 min read

Learn how the Compound Protocol was exploited, which resulted in a loss of $160 million.

TL;DR#

On September 30, 2021, the Compound Protocol was exploited due to a smart contract vulnerability, which resulted in a loss of 490,000 COMP tokens worth approximately $160 million.

Introduction to Compound#

Compound is a decentralized lending protocol.

Vulnerability Assessment#

The root cause of the incident was a coding oversight in the smart contract where a conditional check used the `>` operator instead of the `>=`, leading to unintended COMP token distributions.

Incident Overview#

Compound is governed by its governance token, COMP, and this lending protocol distributes tokens to users under a specific set of conditions. The Comptroller contract on the compound is responsible for distributing yield farming to users.

The major purpose of their Proposal 62 was to split the COMP distribution to liquidity suppliers and borrowers based on governance-set ratios instead of the previous 50/50 share model.

bug was introduced in this proposal in which anyone could add more COMP tokens to the Comptroller by calling the public drip function on the Reservoir vault. As a result, the vault would automatically distribute large amounts of COMP to the wrong addresses.

/**
 * @notice Drips the maximum amount of tokens to match the drip rate since inception
 * @dev Note: this will only drip up to the amount of tokens available.
 * @return The amount of tokens dripped in this call
 */
function drip() public returns (uint) {
  // First, read storage into memory
  EIP20Interface token_ = token;
  uint reservoirBalance_ = token_.balanceOf(address(this)); // TODO: Verify this is a static call
  uint dripRate_ = dripRate;
  uint dripStart_ = dripStart;
  uint dripped_ = dripped;
  address target_ = target;
  uint blockNumber_ = block.number;

  // Next, calculate intermediate values
  uint dripTotal_ = mul(dripRate_, blockNumber_ - dripStart_, "dripTotal overflow");
  uint deltaDrip_ = sub(dripTotal_, dripped_, "deltaDrip underflow");
  uint toDrip_ = min(reservoirBalance_, deltaDrip_);
  uint drippedNext_ = add(dripped_, toDrip_, "tautological");

  // Finally, write new `dripped` value and transfer tokens to target
  dripped = drippedNext_;
  token_.transfer(target_, toDrip_);

  return toDrip_;
}

Due to the decentralized nature of its operations, the protocol would require a governance proposal to pass the patch fix, which was created on October 2, 2021, but got its approval on October 9, 2021.

Consequently, the exploiters were able to transfer 202,472 COMP tokens worth over $68 million at the time of the incident.

These are the details of some of the addresses involved in the heist:

0x98d105874052dDeF7150AFB661190df5f8c3a719 claimed 9,499.48 COMP tokens
0xdf6faDA48f5963a8977eC2BCD7b264434CE294f6 claimed 37,504.95 COMP tokens
0x9657BD100802434797d0fb21c356B0871C24485B claimed 14,995.68 COMP tokens
0x0246C1e74EE54Af89c50BFEfFDDdbad0F0d63da2 claimed 2999.64 COMP tokens

In total, funds worth over $80 million, $22 million, and $45 million, totaling approximately 490,000 COMP tokens, were stolen across multiple transactions, bringing the tally of the overall loss to roughly $160 million.

Incident Analysis#

The Compound protocol upgraded their Comptroller contract to incorporate the changes based on Proposal 62, with a very tiny but devastating bug.

The distributeSupplierComp function of the contract would take in the `cToken` parameter, which is the market (e.g., cETH, cDAI), in which the supplier is interacting, and the `supplier` parameter, which is the address of the supplier to distribute the COMP rewards.

/**
 * @notice Calculate COMP accrued by a supplier and possibly transfer it to them
 * @param cToken The market in which the supplier is interacting
 * @param supplier The address of the supplier to distribute COMP to
 */
function distributeSupplierComp(address cToken, address supplier) internal {
  // TODO: Don't distribute supplier COMP if the user is not in the supplier market.
  // This check should be as gas efficient as possible as distributeSupplierComp is called in many places.
  // - We really don't want to call an external contract as that's quite expensive.

  CompMarketState storage supplyState = compSupplyState[cToken];
  uint supplyIndex = supplyState.index;
  uint supplierIndex = compSupplierIndex[cToken][supplier];

  // Update supplier's index to the current index since we are distributing accrued COMP
  compSupplierIndex[cToken][supplier] = supplyIndex;

  if (supplierIndex == 0 && supplyIndex > compInitialIndex) {
    // Covers the case where users supplied tokens before the market's supply state index was set.
    // Rewards the user with COMP accrued from the start of when supplier rewards were first
    // set for the market.
    supplierIndex = compInitialIndex;
  }

  // Calculate change in the cumulative sum of the COMP per cToken accrued
  Double memory deltaIndex = Double({mantissa: sub_(supplyIndex, supplierIndex)});

  uint supplierTokens = CToken(cToken).balanceOf(supplier);

  // Calculate COMP accrued: cTokenAmount * accruedPerCToken
  uint supplierDelta = mul_(supplierTokens, deltaIndex);

  uint supplierAccrued = add_(compAccrued[supplier], supplierDelta);
  compAccrued[supplier] = supplierAccrued;

  emit DistributedSupplierComp(CToken(cToken), supplier, supplierDelta, supplyIndex);
}

It retrieves the market's current supply state via `supplyState` and the supplier's last recorded index via `supplierIndex` in order to calculate the amount of COMP the supplier has accrued.

If the value of `supplierIndex` is 0 and the current supply index is greater than the initial index of compInitialIndex, it sets the supplier's index to the initial index. This step ensures that suppliers who were present before the market's supply state index was initialized are rewarded from the start of their participation.

A bug appears if and when someone supplies tokens for a market with zero COMP rewards before the market is initialized or migrated. In these cases, the `supplyIndex` value for such tokens remains equal to `compInitialIndex`, which means that the conditional if block on this function is not triggered.

The conditional checks on the if operation should have made use of the `>=` operator instead of the `>` operator. As the if block was not triggered,  the `supplierIndex` remained 0 while the value of `supplyIndex` remained 1e36, and the protocol paid out rewards for 1e36 indexes rather than the intended zero rewards.

Solution#

In the aftermath of the Compound Protocol incident, it became clear that every aspect of protocol optimization and development necessitates meticulous review. A minor oversight, such as the misuse of comparison operators (`>` instead of `>=`), can lead to devastating consequences, highlighting the importance of vigilance in smart contract development. The incident underscores the interconnected nature of smart contracts, where a small change in one area can introduce vulnerabilities elsewhere, making a comprehensive review of upgrades imperative.

This necessitates not only a thorough examination of the changes introduced (delta) but also a full audit of the contract, regardless of the perceived insignificance of an upgrade. Independent third-party audits play a critical role in identifying vulnerabilities that may not be apparent through delta reviews alone, providing a vital layer of security.

Moreover, the specific issue encountered with operator usage in the Compound incident underscores the need for developers to have a heightened awareness of the implications of their coding choices, including operator usage and potential edge cases. Implementing standardized review checklists that include verifying operator usage can help mitigate such risks. Beyond technical measures, fostering a culture of security within development teams and the broader community is essential.

Ultimately, the path to achieving high-security standards in decentralized finance is ongoing and requires the collective effort of developers, auditors, and the community at large. The Compound Protocol incident serves as a stark reminder of the fragility of smart contracts and the critical need for exhaustive security measures. By embracing rigorous review processes, conducting full audits, paying careful attention to code changes, and fostering a culture of security, protocols can significantly reduce the risk of vulnerabilities and protect their users from potential exploits.

Neptune Mutual was not available as a marketplace at the time of the event, so users and the protocol who were collectively affected by this incident had no way of recovering their lost funds. We may not have been able to prevent this hack from occurring, but we could have reduced or mitigated the aftermath of the attack to a greater extent. We are adept at offering coverage for losses due to smart contract vulnerabilities, with parametric policies designed for these particular risks.

Users who would have purchased the available parametric cover policy for Compound need not have to provide loss evidence in order to receive their payout. Payouts would have been made as soon as this type of incident was confirmed and resolved through our comprehensive incident resolution framework. This method ensures swift support for users affected by such security breaches.

At the time of this writing, our marketplace is active on various major blockchain networks, including EthereumArbitrum, and the BNB chain, and provides active coverage for the Compound Protocol. The covers are available for purchase on our marketplace within the Ethereum Mainnet (Compound V2) and Arbitrum network (Compound V3). This extensive coverage enables us to cater to a diverse group of DeFi users, offering them safeguards against potential vulnerabilities.

By

Tags