How Was Citadel Finance Exploited?

5 min read

Learn how Citadel Finance was exploited, resulting in a loss of assets worth $93,000.

TL;DR#

On January 27, 2024, Citadel Finance was exploited on the Arbitrum chain, which resulted in a loss of 43 ETH, worth approximately $93,000.

Introduction to Citadel#

Citadel is a treasury-backed protocol designed to improve sustainable treasury growth and create real yields for stakeholders.

Vulnerability Assessment#

The root cause of the exploit is price manipulation of the underlying assets.

Steps#

Step 1:

We attempt to analyze the attack transaction executed by the exploiter.

Step 2:

The redeem function from the CITRedeem contract uses the getAmountsOut function and relies on the WETH/USDC pair on the Camelot Router to calculate the redeem amount. This pair value could be easily manipulated using a flash loan.

/**
 *
 * @param underlying the id of the underlying to be distributed - 0 for USDC, 1 for ETH
 * @param token the id of the token to be distributed - 0 for CIT, 1 for bCIT
 * @param amount the amount of CIT to be redeemed
 * @param rate the rate, either fixed or variable - 0 for variable, 1 for fixed
 */
function redeem(uint256 underlying, uint256 token, uint256 amount, uint8 rate) public nonReentrant {
  require(underlying == 0 || underlying == 1, "Invalid underlying");
  require(token == 0 || token == 1, "Invalid token");
  require(rate == 0 || rate == 1, "Invalid rate");
  require(amount > 0, "Amount must be greater than 0");

  uint256 amountAvailable = CITStaking.redeemCalculator(msg.sender)[token][rate];
  require(amountAvailable > 0, "Nothing to redeem");

  uint256 amountInUnderlying;
  address tokenAddy = underlying == 0 ? address(USDC) : address(WETH);
  // Variable rate
  if (rate == 0) {
    require(amount <= amountAvailable, "Not enough CIT or bCIT to redeem");
    require(amount <= maxRedeemableVariable, "Amount too high");
    maxRedeemableVariable -= amount;
    address[] memory path = new address[](3);

    path[0] = address(CIT); // 1e18
    path[1] = address(WETH);
    path[2] = address(USDC); // 1e6

    uint[] memory a = camelotRouter.getAmountsOut(amount, path);

    if (underlying == 0) {
      amountInUnderlying = a[2]; // result in 6 decimal
    } else {
      amountInUnderlying = a[1]; // result in 18 decimal
    }
  }
  // Fixed rate
  else {
    uint256 _amount = CITStaking.getCITInUSDAllFixedRates(msg.sender, amount);
    require(amount <= amountAvailable, "Not enough CIT or bCIT to redeem");
    require(amount <= maxRedeemableFixed, "Amount too high");
    maxRedeemableFixed -= amount;
    if (underlying == 1) {
      address[] memory path = new address[](2);

      path[0] = address(USDC); // 1e6
      path[1] = address(WETH); // 1e18

      uint[] memory a = camelotRouter.getAmountsOut(_amount / 1e12, path); // result in 18 decimal

      amountInUnderlying = a[1];
    } else {
      amountInUnderlying = _amount / 1e12; // 1e6 is the decimals of USDC, so 18 - 12 = 6
    }
  }

  if (token == 0) {
    CIT.burn(CITStakingAddy, amount);
    CITStaking.removeStaking(msg.sender, address(CIT), rate, amount);
  } else if (token == 1) {
    totalbCITRedeemedByUser[msg.sender] += amount;
    bCIT.burn(CITStakingAddy, amount);
    CITStaking.removeStaking(msg.sender, address(bCIT), rate, amount);
  }

  treasury.distributeRedeem(tokenAddy, amountInUnderlying, msg.sender);
}

Step 3:

The attacker took a flash loan of approximately 4500 WETH and deposited it with the Camelot LP to manipulate the WETH/USDC.E pool pair.

Step 4:

By calling the redeem function, the exploiter was then able to get huge amounts of WETH from the treasury with a low amount of CIT. Approximately 30.51 CIT tokens were burned, and the attacker took away assets worth 21.326 WETH.

/**
 * @param token the address of the token to be distributed
 * @param amount the amount of the token to be distributed
 * @param user the address of the user to receive the tokens
 */
function distributeRedeem(address token, uint256 amount, address user) public {
  require(msg.sender == CITRedeem, "Only CITRedeem can call this function");
  require(IERC20(token).balanceOf(address(this)) >= amount, "Not enough tokens in treasury");
  if (token == USDC) {
    amountRedeemedUSDC += amount;
  } else if (token == address(WETH)) {
    amountRedeemedETH += amount;
  }
  IERC20(token).transfer(user, amount);
  if (address(this).balance > 0) {
    _wrapETH();
  }
}

Step 5:

The flash loan was repaid, and the attacker was able to retain the excess funds as profit.

Step 6:

The stolen funds were then distributed across four different EOAs and ultimately laundered to FixedFloat.

0xEC151b0D278d3E6Cbc4eB6e7C75f6EC465c1ef02: 12.3345 ETH
0xE2C9024DD69c4Ce80db79B9021e62f2E1BD9c108: 20 ETH
0x581e47c683f57b27aB3A9F3DEB7238AEbf3c815F: 10 ETH
0xf71EC1b096eeCc11dFD2fE44C3ad515067d30845: 1 ETH 

Aftermath#

The team acknowledged the occurrence of the exploit and stated that they would release a detailed postmortem report. The team was reportedly able to save half of the $200,000 worth of assets that were in the Treasury.

According to them, they are working on a new redemption contract that will fix the issue that orchestrated the attack.

Solution#

To address the exploit in Citadel Finance's smart contract, several modifications and improvements are necessary to enhance security and prevent similar vulnerabilities in the future. One of the fundamental changes should involve the implementation of more reliable and manipulation-resistant methods for price determination. Instead of relying solely on the Camelot Router for asset prices, the protocol should use multiple decentralized Oracle services to obtain a more accurate and tamper-proof price feed. By averaging prices from different sources, the protocol can significantly reduce the risk of price manipulation.

In scenarios like these, where price manipulation stands central, it becomes imperative to leverage Oracle systems like ChainLink, which amalgamate data from numerous sources to provide accurate price feeds. Time-weighted average prices (TWAPs) play a pivotal role in ensuring price stability and mitigating abrupt, and likely manipulative, price changes.

Moreover, rigorous testing and auditing of smart contracts are crucial. Before deploying any changes, the contract should undergo extensive testing, including simulations of various attack vectors. Engaging reputable third-party auditors to review the code can help identify and rectify potential vulnerabilities.

Despite having strong security measures, the possibility of vulnerabilities being exploited cannot be entirely eliminated. This is where Neptune Mutual becomes essential. By creating a dedicated cover pool with Neptune Mutual, the adverse effects of events like the Citadel Finance exploit can be significantly mitigated. Neptune Mutual is adept at offering coverage for losses due to smart contract vulnerabilities, with parametric policies designed for these particular risks. Generally, we don't cover losses from price manipulation, but we are open to making exceptions in special circumstances.

Working with Neptune Mutual streamlines the process for users, eliminating the need for them to submit detailed proof of loss. After an incident is confirmed and resolved using our comprehensive incident resolution framework, we quickly focus on providing immediate compensation and financial aid to those impacted. This method ensures swift support for users affected by such security breaches.

Our marketplace is active on various major blockchain networks, including EthereumArbitrum, and the BNB chain. This extensive coverage enables us to cater to a diverse group of DeFi users, offering them safeguards against potential vulnerabilities.

Reference Source Inspex

By

Tags