How Was Arcadia Finance Exploited?

7 min read

Learn how Arcadia Finance was exploited, resulting in a loss of funds worth $459,030.

TL;DR#

On July 10, 2023, Arcadia Finance was exploited on the Ethereum and Optimism chains, resulting in a total loss of approximately $459,030.

Introduction to Arcadia Finance#

Arcadia is a non-custodial protocol that supports on-chain cross-margin accounts, enabling users to leverage their portfolios and interact with multiple protocols.

Vulnerability Assessment#

The root cause of the vulnerability is a lack of reentrancy protection, which caused the liquidation of funds from two of their vaults.

Steps#

Step 1:

We attempt to analyze the attack transaction executed by this exploiter on the Ethereum Mainnet and another transaction on the Optimism chain by this exploiter.

Step 2:

On one of the vulnerable contracts, the check on the collateral value can be bypassed, allowing for instant liquidation to bypass the internal vault health check.

function vaultManagementAction(address actionHandler, bytes calldata actionData)
  external
  onlyAssetManager
  returns (address, uint256)
{
  require(IMainRegistry(registry).isActionAllowed(actionHandler), "V_VMA: Action not allowed");

  (ActionData memory outgoing,,,) = abi.decode(actionData, (ActionData, ActionData, address[], bytes[]));

  // Withdraw assets to actionHandler.
  _withdraw(outgoing.assets, outgoing.assetIds, outgoing.assetAmounts, actionHandler);

  // Execute Action(s).
  ActionData memory incoming = IActionBase(actionHandler).executeAction(actionData);

  // Deposit assets from actionHandler into vault.
  _deposit(incoming.assets, incoming.assetIds, incoming.assetAmounts, actionHandler);

  //If usedMargin is equal to fixedLiquidationCost, the open liabilities are 0 and the Vault is always in a healthy state.
  uint256 usedMargin = getUsedMargin();
  if (usedMargin > fixedLiquidationCost) {
    //Vault must be healthy after actions are executed.
    require(getCollateralValue() >= usedMargin, "V_VMA: Vault Unhealthy");
  }

  return (trustedCreditor, vaultVersion);
}

Step 3:

The attacker is able to reenter the `liquidateVault` function to liquidate the funds from two of their darcWETH and darcUSDC vaults. The global variable `isTrustedCreditorSet` will be set to `false`, thus bypassing the collateral checks.

function liquidateVault(uint256 openDebt)
  external
  returns (address originalOwner, address baseCurrency_, address trustedCreditor_)
{
  require(msg.sender == liquidator, "V_LV: Only Liquidator");

  //Cache trustedCreditor.
  trustedCreditor_ = trustedCreditor;

  //Close margin account.
  isTrustedCreditorSet = false;
  trustedCreditor = address(0);
  liquidator = address(0);

  //If getLiquidationValue (total value discounted with liquidation factor to account for slippage)
  //is smaller than the Used Margin: sum of the liabilities of the Vault (openDebt)
  //and the max gas cost to liquidate the vault (fixedLiquidationCost),
  //then the Vault can be successfully liquidated.
  //Liquidations are triggered by the trustedCreditor (via Liquidator), the openDebt is
  //passed as input to avoid the need of another contract call back to trustedCreditor.
  require(getLiquidationValue() < openDebt + fixedLiquidationCost, "V_LV: liqValue above usedMargin");

  //Set fixedLiquidationCost to 0 since margin account is closed.
  fixedLiquidationCost = 0;

  //Transfer ownership of the ERC721 in Factory of the Vault to the Liquidator.
  IFactory(IMainRegistry(registry).factory()).liquidate(msg.sender);

  //Transfer ownership of the Vault itself to the Liquidator.
  originalOwner = owner;
  _transferOwnership(msg.sender);

  emit TrustedMarginAccountChanged(address(0), address(0));

  return (originalOwner, baseCurrency, trustedCreditor_);
}
function getUsedMargin() public view returns (uint256 usedMargin) {
  if (!isTrustedCreditorSet) return 0;

  //getOpenPosition() is a view function, cannot modify state.
  usedMargin = ITrustedCreditor(trustedCreditor).getOpenPosition(address(this)) + fixedLiquidationCost;
}

Step 4:

The exploiter on the Ethereum Mainnet took a flash loan of 2.4 WETH and 20,672 USDC before depositing them into the ArcadiaFi protocol. They then called the `doActionWithLeverage` function to expand the held funds by five times and deposit them into the ActionMultiCall contract.

function doActionWithLeverage(
  uint256 amountBorrowed,
  address vault,
  address actionHandler,
  bytes calldata actionData,
  bytes3 referrer
) external whenBorrowNotPaused processInterests {
  //If vault is not an actual address of a vault, ownerOfVault(address) will return the zero address.
  address vaultOwner = IFactory(vaultFactory).ownerOfVault(vault);
  require(vaultOwner != address(0), "LP_DAWL: Not a vault");

  uint256 amountBorrowedWithFee = amountBorrowed + (amountBorrowed * originationFee) / 10_000;

  //Check allowances to take debt.
  if (vaultOwner != msg.sender) {
    //Since calling vaultManagementAction() gives the sender full control over all assets in the vault,
    //Only Beneficiaries with maximum allowance can call the doActionWithLeverage function.
    require(creditAllowance[vault][vaultOwner][msg.sender] == type(uint256).max, "LP_DAWL: UNAUTHORIZED");
  }

  //Mint debt tokens to the vault, debt must be minted Before the actions in the vault are performed.
  _deposit(amountBorrowedWithFee, vault);

  //Add origination fee to the treasury.
  unchecked {
    totalRealisedLiquidity += SafeCastLib.safeCastTo128(amountBorrowedWithFee - amountBorrowed);
    realisedLiquidityOf[treasury] += amountBorrowedWithFee - amountBorrowed;
  }

  //Send Borrowed funds to the actionHandler.
  asset.safeTransfer(actionHandler, amountBorrowed);

  //The actionHandler will use the borrowed funds (optionally with additional assets withdrawn from the Vault)
  //to execute one or more actions (swap, deposit, mint...).
  //Next the actionHandler will deposit any of the remaining funds or any of the recipient token
  //resulting from the actions back into the vault.
  //As last step, after all assets are deposited back into the vault a final health check is done:
  //The Collateral Value of all assets in the vault is bigger than the total liabilities against the vault (including the margin taken during this function).
  (address trustedCreditor, uint256 vaultVersion) = IVault(vault).vaultManagementAction(actionHandler, actionData);
  require(trustedCreditor == address(this) && isValidVersion[vaultVersion], "LP_DAWL: Reverted");

  emit Borrow(vault, msg.sender, actionHandler, amountBorrowed, amountBorrowedWithFee - amountBorrowed, referrer);
}

Step 5:

The `actionData` parameter in the `executeAction` function also had weak validations, which was exploited by the attacker to approve WETH and USDC to a malicious contract with the permission of the ActionMultiCall contract and then call yet another `transferFrom` function to transfer these approved funds.

function executeAction(bytes calldata actionData) external override returns (ActionData memory) {
  (, ActionData memory incoming, address[] memory to, bytes[] memory data) =
    abi.decode(actionData, (ActionData, ActionData, address[], bytes[]));

  uint256 callLength = to.length;

  require(data.length == callLength, "EA: Length mismatch");

  for (uint256 i; i < callLength;) {
    (bool success, bytes memory result) = to[i].call(data[i]);
    require(success, string(result));

    unchecked {
      ++i;
    }
  }

  for (uint256 i; i < incoming.assets.length;) {
    if (incoming.assetTypes[i] == 0) {
      incoming.assetAmounts[i] = IERC20(incoming.assets[i]).balanceOf(address(this));
    } else if (incoming.assetTypes[i] == 1) {
      incoming.assetAmounts[i] = 1;
    } else if (incoming.assetTypes[i] == 2) {
      incoming.assetAmounts[i] = IERC1155(incoming.assets[i]).balanceOf(address(this), incoming.assetIds[i]);
    }
    unchecked {
      ++i;
    }
  }

  return incoming;
}

Step 6:

The contract checked whether the funds were normal after transferring, and the attacker re-entered the leveraged contract to perform liquidation to bypass the check, thus taking away their share of rewards.

Step 7:

The attacker on the Ethereum chain made a profit of 11.25 ETH and 103,194 DAI, totaling approximately $124,230, all of which are currently held at this address.

Step 8:

The attacker on the Optimism chain withdrew 148.23 ETH and 59,427 USDC to the Ethereum chain, and then finally transferred all profit worth 179.30 ETH to Tornado Cash.

Aftermath#

Following the incident, the team acknowledged the occurrence of the exploit and stated that they have paused the contracts and are investigating the root cause with security experts.

The team also sent an on-chain message to the attacker on both the Ethereum and Optimism chains with a proposal to return 90% of the stolen funds within the next 24 hours and walk away with the remaining 10% of the remaining funds as bounty rewards.

A post-mortem report was also shared by the team, which suggests that the financial impact from the exploit on the Ethereum chain is 103,200.65 USDC and 11.047 ETH, while the loss on the Optimism chain is 59,427.42 USDC and 148.22 ETH, totaling approximately $459,030.

Solution#

The recent exploitation of Arcadia Finance underlines the critical need for robust security measures, thorough code reviews, and extensive testing within the DeFi ecosystem. This incident also illustrates how indispensable solutions like Neptune Mutual can be in reducing the impact of such incidents.

The attack on Arcadia Finance was executed through a reentrancy attack, which exploited the lack of a reentrancy guard in the contract code. The attacker was able to repeatedly call the `liquidateVault` function and bypass collateral checks, leading to significant losses.

To prevent such attacks, smart contract developers should employ comprehensive security practices. These include utilizing mutex to make the function of a smart contract non-reentrant and employing a checks-effects-interactions pattern to ensure internal state changes precede calls to external contracts. These practices can reduce the likelihood of a reentrancy attack.

In response to the exploit, the Arcadia Finance team has indicated that they will introduce a reentrancy guard in their Vault contract, particularly in the `liquidateVault` function. This is a step in the right direction. Nevertheless, it is equally important to conduct rigorous smart contract testing, which includes unit testing, integration testing, and functional testing. Such tests can identify potential vulnerabilities and weaknesses before deployment, allowing developers to rectify them proactively.

As a part of Neptune Mutual, we understand that despite taking all these preventive measures, the risk of attack cannot be entirely eliminated. That's why we offer our users the ability to safeguard their investments through our cover policies.

Had Arcadia Finance established a dedicated cover pool with Neptune Mutual prior to this exploit, the users' losses could have been significantly mitigated. Our coverage provides protection for users who have suffered losses due to smart contract vulnerabilities, and our innovative parametric policies remove the need for users to prove their losses. Once an incident has been confirmed and resolved through our incident resolution system, the affected users can claim their coverage immediately.

Our marketplace is available on several popular blockchain networks including EthereumArbitrum, and the BNB chain, offering a broad reach to serve a diverse array of DeFi users. Our solution not only provides immediate financial relief in the aftermath of an incident, but it also boosts the overall confidence in the DeFi ecosystem.

Furthermore, Neptune Mutual's security team conducts thorough security assessments of platforms, looking into a range of potential vulnerabilities. Our evaluations cover various aspects like DNS and web-based security, frontend and backend security, and intrusion detection and prevention. These in-depth assessments can yield valuable insights into potential weaknesses, guiding platforms to enhance their security.

Reference Sources PeckShieldArcadia

By

Tags