How Was Abracadabra Money Exploited?

6 min read

Learn how Abracadabra Money was exploited, resulting in a loss of assets worth $6.5 million.

TL;DR#

On January 30, 2024, Abracadabra Money was exploited on the Ethereum Mainnet due to a smart contract vulnerability, which resulted in a loss of over 2740 ETH, including 2.2 million MIM tokens worth approximately $6.5 million.

Introduction to Abracadabra Money#

Abracadabra Money is a leverage and lending platform that uses interest-bearing tokens as collateral to borrow the omnistablecoin MIM.

Vulnerability Assessment#

The root cause of the exploit is a loss of precision during smart contract operations.

Steps#

Step 1:

We attempt to analyze one of the attack transactions executed by the exploiter.

Step 2:

The exploit targeted specific Cauldrons V3 and V4, allowing for the unauthorized borrowing of MIM tokens.

Step 3:

According to the contract's implementation, the `part` value represents the borrowed amount of the user's share in the total debt.

/// @dev Concrete implementation of `borrow`.
function _borrow(address to, uint256 amount) internal returns (uint256 part, uint256 share) {
  uint256 feeAmount = amount.mul(BORROW_OPENING_FEE) / BORROW_OPENING_FEE_PRECISION; // A flat % fee is charged for any borrow
  (totalBorrow, part) = totalBorrow.add(amount.add(feeAmount), true);

  BorrowCap memory cap = borrowLimit;

  require(totalBorrow.elastic <= cap.total, "Borrow Limit reached");

  accrueInfo.feesEarned = accrueInfo.feesEarned.add(uint128(feeAmount));

  uint256 newBorrowPart = userBorrowPart[msg.sender].add(part);
  require(newBorrowPart <= cap.borrowPartPerAddress, "Borrow Limit reached");
  _preBorrowAction(to, amount, newBorrowPart, part);

  userBorrowPart[msg.sender] = newBorrowPart;

  // As long as there are tokens on this contract you can 'mint'... this enables limiting borrows
  share = bentoBox.toShare(magicInternetMoney, amount, false);
  bentoBox.transfer(magicInternetMoney, address(this), to, share);

  emit LogBorrow(msg.sender, to, amount.add(feeAmount), part);
}

Step 4:

The attacker took a flash loan of MIM tokens from `Abracadabra.Money: Degenbox`, and then donated these tokens to BentoBox.

Step 5:

The attacker managed to repay the debt of all other users manually and also by invoking a call to the repayForAll function, thus setting the `totalBorrow.elastic` value to zero, thereby bypassing the security checks.

Due to the rounding errors, the value of `totalBorrow.base` didn't drop to zero but stayed at a valid integer range, leading to possible manipulation.

/// @notice Used to auto repay everyone liabilities'.
/// Transfer MIM deposit to DegenBox for this Cauldron and increase the totalBorrow base or skim
/// all mim inside this contract
function repayForAll(uint128 amount, bool skim) public returns (uint128) {
  accrue();

  if (skim) {
    // ignore amount and take every mim in this contract since it could be taken by anyone, the next block.
    amount = uint128(magicInternetMoney.balanceOf(address(this)));
    bentoBox.deposit(magicInternetMoney, address(this), address(this), amount, 0);
  } else {
    bentoBox.transfer(magicInternetMoney, msg.sender, address(this), bentoBox.toShare(magicInternetMoney, amount, true));
  }

  uint128 previousElastic = totalBorrow.elastic;

  require(previousElastic - amount > 1000 * 1e18, "Total Elastic too small");

  totalBorrow.elastic = previousElastic - amount;

  emit LogRepayForAll(amount, previousElastic, totalBorrow.elastic);
  return amount;
}

Step 6:

This discrepancy between the values of `totalBorrow.elastic` and `totalBorrow.base` wasn't accounted for by the Rebase library. As a result, the attacker was able to borrow and repay a single token repeatedly, due to which the value of `part` rose to a very large number.

/// @notice Calculates the base value in relationship to `elastic` and `total`.
function toBase(Rebase memory total, uint256 elastic, bool roundUp) internal pure returns (uint256 base) {
  if (total.elastic == 0) {
    base = elastic;
  } else {
    base = (elastic * total.base) / total.elastic;
    if (roundUp && (base * total.elastic) / total.base < elastic) {
      base++;
    }
  }
}

/// @notice Calculates the elastic value in relationship to `base` and `total`.
function toElastic(Rebase memory total, uint256 base, bool roundUp) internal pure returns (uint256 elastic) {
  if (total.base == 0) {
    elastic = base;
  } else {
    elastic = (base * total.elastic) / total.base;
    if (roundUp && (elastic * total.base) / total.elastic < base) {
      elastic++;
    }
  }
}

Step 7:

Consequently, the final debt or the solvency checks implemented by the protocol were redundant. The borrowed part value was extremely negligible in comparison to the total debt parts fed using `_totalBorrow.base`, allowing the attacker to drain all the liquidity from the pool.

/// @notice Concrete implementation of `isSolvent`. Includes a third parameter to allow caching `exchangeRate`.
/// @param _exchangeRate The exchange rate. Used to cache the `exchangeRate` between calls.
function _isSolvent(address user, uint256 _exchangeRate) internal view returns (bool) {
  // accrue must have already been called!
  uint256 borrowPart = userBorrowPart[user];
  if (borrowPart == 0) return true;
  uint256 collateralShare = userCollateralShare[user];
  if (collateralShare == 0) return false;

  Rebase memory _totalBorrow = totalBorrow;

  return
    bentoBox.toAmount(collateral, collateralShare.mul(EXCHANGE_RATE_PRECISION / COLLATERIZATION_RATE_PRECISION).mul(COLLATERIZATION_RATE), false) >=
    // Moved exchangeRate here instead of dividing the other side to preserve more precision
    borrowPart.mul(_totalBorrow.elastic).mul(_exchangeRate) / _totalBorrow.base;
}

Step 8:

These are the addresses of the exploiters involved in the heist:

0x87F585809Ce79aE39A5fa0C7C96d0d159eb678C9, tagged as AM. Exploiter
0x40d5FFA20fC0dF6bE4D9991938dAa54E6919c714, tagged as AM. Exploiter 2
0xbD12D6054827ae3fc6D23B1aCf47736691b52Fd3, tagged as AM. Exploiter 3

Step 9:

At the time of this writing, the address tagged as `AM. Exploiter 2`, has held 1800 ETH, worth approximately $4,207,788, and `AM. Exploiter 3`, has held 939.2662 ETH, worth approximately $2,194,765.

Aftermath#

The team acknowledged the occurrence of the exploit and stated that they were triaging and investigating the situation. The DAO Treasury will buy back MIM from the market and then burn it. According to them, the exploit targeted specific Cauldron V3 and V4 contracts, which has been mitigated by setting borrowing limits to zero for the affected cauldrons.

The team has also sent an on-chain message to the exploiter, with hopes of recovering the stolen assets.

Solution#

To address the exploit in Abracadabra Money's smart contract, several modifications and improvements are necessary to enhance security and prevent similar vulnerabilities in the future. The foremost priority is rectifying the smart contract vulnerability that led to precision loss during operations, a crucial factor in this exploit.

Central to this approach is the implementation of robust and high-precision mathematical functions within smart contracts. Developers should utilize libraries or frameworks specifically crafted for secure arithmetic operations in these environments. This is crucial for ensuring accurate handling of mathematical operations such as division, multiplication, and rounding, where precision loss could be exploited.

Precision in numerical operations is particularly vital in scenarios involving ratios, rates, or percentages, where even slight inaccuracies can have significant implications. To accommodate this, it's advisable to use larger numerators in fractions. Moreover, careful consideration must be given to the order of operations, favoring multiplication before division wherever possible to preserve precision. A prudent strategy is to temporarily elevate variables to higher precision during calculations, then scale back to the required precision level for final values.

Given Solidity's limitation in not supporting floating-point numbers, developers often employ fixed-point arithmetic for handling decimal numbers. This technique involves scaling values (e.g., multiplying Ether values by 10^18), performing operations in this scaled integer format, and then appropriately scaling down to obtain the final results. While this method effectively maintains precision, it requires diligent management of scaling factors. The strategic use of fixed-point arithmetic is thus essential in maintaining precision and avoiding potential risks, reinforcing its importance in developing secure and reliable smart contracts.

At Neptune Mutual, we understand that, despite implementing strong security measures, the complete elimination of vulnerabilities being exploited is a challenge. This is where our services come into play. By creating a dedicated cover pool with Neptune Mutual, the adverse effects of events like the Abracadabra Money exploit could have been significantly mitigated. We specialize in offering coverage for losses due to smart contract vulnerabilities, courtesy of our parametric-based policies crafted specifically for these types of risks.

Collaborating with us simplifies the process for users, eliminating the need for them to submit detailed proof of loss. Once an incident is confirmed and resolved through our comprehensive incident resolution framework, our priority shifts to providing immediate compensation and financial aid to those impacted. This approach guarantees prompt support for users affected by such security breaches, ensuring they receive timely assistance.

Our marketplace operates on various major blockchain networks, including EthereumArbitrum, and the BNB chain. This extensive network presence allows us to serve a wide range of DeFi users, offering them robust safeguards against potential vulnerabilities. 

Reference Source Hacken

By

Tags