Understanding Midas Capital Exploit

5 min read
Midas capital read only reentrancy attack analysis

Learn how an attacker exploited Midas Capital to steal funds worth approximately $600,000.

TL;DR#

On June 17, 2023, Midas Capital was exploited on the BNB chain due to a smart contract vulnerability, which resulted in a loss of funds worth approximately $600,000.

Introduction to Midas Capital#

Midas Capital is a cross-chain money market solution that unlocks and maximizes the usage of all digital assets.

Vulnerability Assessment#

The root cause of the exploit is due to the rounding issue in its lending protocol, which was forked from the V2 code base of Compound Finance.

Steps#

Step 1:

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

Step 2:

The attack is similar to the earlier exploit on Hundred Finance, in which the exploiter targeted empty pools that lacked lending activity, thereby gaining control over the liquidity.

Step 3:

This contract and its associated implementation contract misused the redeem counter, which allowed for multiple redemptions via the mint function based on the token amounts. The attacker manipulated the exchange rate due to a bug involving division calculation on the redeem tokens operation.

/**
 * @notice User redeems cTokens in exchange for the underlying asset
 * @dev Assumes interest has already been accrued up to the current block
 * @param redeemer The address of the account which is redeeming the tokens
 * @param redeemTokensIn The number of cTokens to redeem into underlying
 *    (only one of redeemTokensIn or redeemAmountIn may be non-zero)
 * @param redeemAmountIn The number of underlying tokens to receive from redeeming cTokens
 *    (only one of redeemTokensIn or redeemAmountIn may be non-zero)
 * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details)
 */
function redeemFresh(address redeemer, uint256 redeemTokensIn, uint256 redeemAmountIn) internal returns (uint256) {
  require(redeemTokensIn == 0 || redeemAmountIn == 0, "!redeemTokensInorOut!=0");

  RedeemLocalVars memory vars;

  /* exchangeRate = invoke Exchange Rate Stored() */
  (vars.mathErr, vars.exchangeRateMantissa) = exchangeRateStoredInternal();
  if (vars.mathErr != MathError.NO_ERROR) {
    return failOpaque(Error.MATH_ERROR, FailureInfo.REDEEM_EXCHANGE_RATE_READ_FAILED, uint256(vars.mathErr));
  }

  if (redeemAmountIn == type(uint256).max) {
    redeemAmountIn = comptroller.getMaxRedeemOrBorrow(redeemer, address(this), false);
  }

  /* If redeemTokensIn > 0: */
  if (redeemTokensIn > 0) {
    /*
      * We calculate the exchange rate and the amount of underlying to be redeemed:
      *  redeemTokens = redeemTokensIn
      *  redeemAmount = redeemTokensIn x exchangeRateCurrent
      */
    vars.redeemTokens = redeemTokensIn;

    (vars.mathErr, vars.redeemAmount) = mulScalarTruncate(Exp({mantissa: vars.exchangeRateMantissa}), redeemTokensIn);
    if (vars.mathErr != MathError.NO_ERROR) {
      return
        failOpaque(Error.MATH_ERROR, FailureInfo.REDEEM_EXCHANGE_TOKENS_CALCULATION_FAILED, uint256(vars.mathErr));
    }
  } else {
    /*
      * We get the current exchange rate and calculate the amount to be redeemed:
      *  redeemTokens = redeemAmountIn / exchangeRate
      *  redeemAmount = redeemAmountIn
      */

    (vars.mathErr, vars.redeemTokens) =
      divScalarByExpTruncate(redeemAmountIn, Exp({mantissa: vars.exchangeRateMantissa}));
    if (vars.mathErr != MathError.NO_ERROR) {
      return
        failOpaque(Error.MATH_ERROR, FailureInfo.REDEEM_EXCHANGE_AMOUNT_CALCULATION_FAILED, uint256(vars.mathErr));
    }

    vars.redeemAmount = redeemAmountIn;
  }

  /* Fail if redeem not allowed */
  uint256 allowed = comptroller.redeemAllowed(address(this), redeemer, vars.redeemTokens);
  if (allowed != 0) {
    return failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.REDEEM_COMPTROLLER_REJECTION, allowed);
  }

  /* Verify market's block number equals current block number */
  if (accrualBlockNumber != getBlockNumber()) {
    return fail(Error.MARKET_NOT_FRESH, FailureInfo.REDEEM_FRESHNESS_CHECK);
  }

  /*
    * We calculate the new total supply and redeemer balance, checking for underflow:
    *  totalSupplyNew = totalSupply - redeemTokens
    *  accountTokensNew = accountTokens[redeemer] - redeemTokens
    */
  (vars.mathErr, vars.totalSupplyNew) = subUInt(totalSupply, vars.redeemTokens);
  if (vars.mathErr != MathError.NO_ERROR) {
    return failOpaque(Error.MATH_ERROR, FailureInfo.REDEEM_NEW_TOTAL_SUPPLY_CALCULATION_FAILED, uint256(vars.mathErr));
  }

  (vars.mathErr, vars.accountTokensNew) = subUInt(accountTokens[redeemer], vars.redeemTokens);
  if (vars.mathErr != MathError.NO_ERROR) {
    return
      failOpaque(Error.MATH_ERROR, FailureInfo.REDEEM_NEW_ACCOUNT_BALANCE_CALCULATION_FAILED, uint256(vars.mathErr));
  }

  /* Fail gracefully if protocol has insufficient cash */
  if (getCashPrior() < vars.redeemAmount) {
    return fail(Error.TOKEN_INSUFFICIENT_CASH, FailureInfo.REDEEM_TRANSFER_OUT_NOT_POSSIBLE);
  }

  /////////////////////////
  // EFFECTS & INTERACTIONS
  // (No safe failures beyond this point)

  /* We write previously calculated values into storage */
  totalSupply = vars.totalSupplyNew;
  accountTokens[redeemer] = vars.accountTokensNew;

  /*
    * We invoke doTransferOut for the redeemer and the redeemAmount.
    *  Note: The cToken must handle variations between ERC-20 and ETH underlying.
    *  On success, the cToken has redeemAmount less of cash.
    *  doTransferOut reverts if anything goes wrong, since we can't be sure if side effects occurred.
    */
  doTransferOut(redeemer, vars.redeemAmount);

  /* We emit a Transfer event, and a Redeem event */
  emit Transfer(redeemer, address(this), vars.redeemTokens);
  emit Redeem(redeemer, vars.redeemAmount, vars.redeemTokens);

  /* We call the defense hook */
  comptroller.redeemVerify(address(this), redeemer, vars.redeemAmount, vars.redeemTokens);

  return uint256(Error.NO_ERROR);
}

Step 4:

The exploiter initially took a flash loan of ANKR-like tokens and also deposited other assets to acquire vouchers before transferring all of them into the pool. Due to this, the voucher values inflated, augmenting the borrowing capacity.

Step 5:

Essentially, when the attacker provided different amounts of HAY/BUSD tokens, they were able to redeem the tokens in multiple folds of their initial supply.

Step 6:

The exploiter has already laundered assets worth approximately 510 BNB to Tornado Cash, while some parts of the profits are also bridged to Ethereum.

Aftermath#

Following the incident, the team acknowledged the occurrence of the exploit in one of their pools. The incident was only isolated to a single pool; however, they have paused all of their pools until they have a reasonable conclusion to the root cause of the attack.

Solution#

The nature of the exploit highlights the need for more extensive testing of the smart contract, which might have detected this vulnerability. A multi-dimensional approach to testing, including unit, integration, and functional testing, would be essential to identifying such potential issues before deploying the contract.

Moreover, the use of formal verification tools would have helped confirm that the smart contract performed as expected. Given the contract's complexity, third-party audits by independent and experienced auditors should be considered to uncover potential vulnerabilities and suggest measures to mitigate them.

However, even the most meticulous security measures can fail to prevent all attacks. Therefore, there's a need for robust financial protections to limit the impact of an exploit once it happens. This is where Neptune Mutual could play a crucial role. If the team associated with Midas Capital had established a dedicated cover pool in the Neptune Mutual marketplace, the aftermath of this attack could have been significantly lessened.

Neptune Mutual offers parametric cover policies, which provide coverage to users for the loss of digital assets due to smart contract vulnerabilities. The payout from such policies does not require evidence of loss and can be claimed as soon as an incident is resolved. At the moment, the marketplace is available on two popular blockchain networks, Ethereum, and Arbitrum.

Neptune Mutual also assists in strengthening the platform's security against potential threats by evaluating aspects like DNS and web-based security, frontend and backend security, and intrusion detection and prevention measures. These security assessments are crucial to not only preventing exploits but also ensuring that any potential issue is addressed promptly to limit its impact.

Reference Source Ancilia

By

Tags