Analysis of the Stars Arena Exploit

8 min read

Learn how Stars Arena was exploited, resulting in a loss of funds worth $2.9 million.

TL;DR#

On October 7, 2023, Stars Arena was exploited due to a smart contract vulnerability, which resulted in a loss of funds worth approximately $2,974,530.

Introduction to Stars Arena#

Stars Arena is a clone of friend.tech that apparently cites itself as the future of SocialFi on the Avalanche blockchain.

Vulnerability Assessment#

The root cause of the exploit is due to the reentrancy vulnerability.

Steps#

Step 1:

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

Step 2:

The first exploit was vested on the `getPrice` function of the contract, which required 4 times the profit in gas to drain the contract. This attack resulted in a loss of approximately $2000 before publicizing the attack vector.

function getPrice(address varg0, uint256 varg1, uint256 varg2) public nonPayable {
  require(msg.data.length - 4 >= 96);
  require(varg0 == varg0);
  v0 = 0x1a9b(varg2, varg1, varg0);
  return v0;
}

Step 3:

The team announced a fix to this issue, and the StarsArena deployer deployed a new contract and pointed the proxy to it.

Step 4:

The associated contract for the second exploit was not public; therefore, we decompile the bytecode of the contract using the publicly available details.

There appears to be a `0x5632b2e4` function, which takes in four uint256 arguments varg0, varg1, varg2, and varg3, performs some preliminary conditional checks, and then updates the corresponding `owner` mapping with the functional arguments of weights.

function 0x5632b2e4(uint256 varg0, uint256 varg1, uint256 varg2, uint256 varg3) public nonPayable { 
  require(msg.data.length - 4 >= 128);
  require(!uint8(owner_a1[msg.sender]), Error('Weights already initialized'));
  require(!owner_a7[msg.sender].field0.length, Error("Can't change weights after shares have been issued"));
  require(varg0 > 0, Error('Weight A must be greater than 0'));
  require(varg1 > 0, Error('Weight A must be greater than 0'));
  require(varg2 > 0, Error('Weight C must be greater than 0'));
  owner_9d[msg.sender] = varg0;
  owner_9e[msg.sender] = varg1;
  owner_9f[msg.sender] = varg2;
  owner_a0[msg.sender] = varg3;
  owner_a1[msg.sender] = 0x1 | bytes31(owner_a1[msg.sender]);
}

Step 5:

There is yet another function `0xe9ccf3a3` in the contract that takes in two address arguments, varg0 and varg2, and an unsigned integer argument, varg1.

This function performs some redundant checks and then invokes a call to the `0x326c` function if the `varg2` argument is a non-zero address. It also calls the `0x2058` function with varg1 and varg0 parameters.

function 0xe9ccf3a3(address varg0, uint256 varg1, address varg2) public payable { 
  require(msg.data.length - 4 >= 96);
  require(varg0 == varg0);
  require(varg2 == varg2);
  if (varg2) {
      0x326c(varg2, msg.sender);
  }
  0x2058(varg1, varg0);
}

Step 6:

When the function `0x2058` is invoked, it transfers the functional call to yet another `0x1a9b` function of the contract.

function 0x2058(uint256 varg0, uint256 varg1) private { 
    v0 = 0x1a9b(varg0, _sharesSupply[address(varg1)], varg1);
    v1 = _SafeMul(v0, _protocolFeePercent);
    require(0xde0b6b3a7640000, Panic(18)); // division by zero
    v2 = _SafeMul(v0, _subjectFeePercent);
    require(0xde0b6b3a7640000, Panic(18)); // division by zero
    v3 = _SafeMul(v0, _referralFeePercent);
    require(0xde0b6b3a7640000, Panic(18)); // division by zero
    v4 = _SafeAdd(v0, v1 / 0xde0b6b3a7640000);
    v5 = _SafeAdd(v4, v2 / 0xde0b6b3a7640000);
    v6 = _SafeAdd(v5, v3 / 0xde0b6b3a7640000);
    require(msg.value >= v6, Error('Insufficient payment'));
    v7 = _SafeAdd(_getMyShares[address(varg1)][msg.sender], varg0);
    _getMyShares[address(varg1)][msg.sender] = v7;
    v8 = _SafeAdd(_sharesSupply[address(varg1)], varg0);
    _sharesSupply[address(varg1)] = v8;
    v9 = 0x1a9b(1, _sharesSupply[address(varg1)], varg1);
    v10 = _SafeAdd(_sharesSupply[address(varg1)], varg0);
    0x307c(v1 / 0xde0b6b3a7640000);
    0x30ef(v2 / 0xde0b6b3a7640000, varg1);
    v11 = _SafeAdd(v0, v1 / 0xde0b6b3a7640000);
    v12 = _SafeAdd(v11, v2 / 0xde0b6b3a7640000);
    v13 = _SafeAdd(v12, v3 / 0xde0b6b3a7640000);
    v14 = _SafeSub(msg.value, v13);
    if (v14) {
        0x30ef(v14, msg.sender);
    }
    if (v3 / 0xde0b6b3a7640000) {
        0x2f7b(v3 / 0xde0b6b3a7640000, msg.sender);
    }
    if (varg0 == _getMyShares[address(varg1)][msg.sender]) {
        v15 = address(varg1);
        owner_a7[v15].field0.length = owner_a7[v15].field0.length + 1;
        owner_a7[v15].field0[owner_a7[v15].field0.length].field0 = msg.sender | bytes12(owner_a7[v15].field0[owner_a7[v15].field0.length].field0);
    }
    emit 0xc9d4f93ded9b42fa24561e02b2a40f720f71601eb1b3f7b3fd4eff20877639ee(msg.sender, address(varg1), bool(1), varg0, v0, v1 / 0xde0b6b3a7640000, v2 / 0xde0b6b3a7640000, v3 / 0xde0b6b3a7640000, v10, v9, _getMyShares[address(varg1)][msg.sender]);
    return ;
}

Step 7:

During the call of the `0xe9ccf3a3` function, the attacker is able to reenter and call the `0x5632b2e4` function, thereby setting a block height.

function 0x1a9b(uint256 varg0, uint256 varg1, uint256 varg2) private { 
  v0 = 0x2329(varg2);
  v1 = _SafeAdd(varg1, v0);
  if (v1) {
    v2 = _SafeSub(v1, 1);
    v3 = _SafeMul(2, v2);
    v4 = _SafeAdd(1, v3);
    v5 = _SafeSub(v1, 1);
    v6 = _SafeMul(v5, v1);
    v7 = _SafeMul(v6, v4);
    require(6, Panic(18)); // division by zero
    v8 = _SafeSub(v1, 1);
    v9 = _SafeAdd(v8, varg0);
    v10 = _SafeMul(2, v9);
    v11 = _SafeAdd(1, v10);
    v12 = _SafeAdd(v1, varg0);
    v13 = _SafeSub(v1, 1);
    v14 = _SafeAdd(v13, varg0);
    v15 = _SafeMul(v14, v12);
    v16 = _SafeMul(v15, v11);
    require(6, Panic(18)); // division by zero
    v17 = 0xfd5(varg2);
    v18 = _SafeSub(v16 / 6, v7 / 6);
    v19 = 0xeeb(varg2);
    v20 = _SafeMul(v19, v18);
    require(0xde0b6b3a7640000, Panic(18)); // division by zero
    v21 = _SafeAdd(v20 / 0xde0b6b3a7640000, v17);
    v22 = 0x1840(varg2);
    v23 = _SafeMul(v22, v21);
    v24 = _SafeMul(v23, _initialPrice);
    require(0xde0b6b3a7640000, Panic(18)); // division by zero
    if (v24 / 0xde0b6b3a7640000 >= _initialPrice) {
        return v24 / 0xde0b6b3a7640000;
    } else {
        return _initialPrice;
    }
  } else {
      return _initialPrice;
  }
}

Step 8:

Then, in the sellShares function of the contract, this height value was used as a parameter to calculate the amount of AVAX to send, resulting in an abnormally large calculated amount.

function sellShares(address varg0, uint256 varg1) public payable {
  require(msg.data.length - 4 >= 64);
  require(varg0 == varg0);
  v0 = _SafeSub(_sharesSupply[varg0], varg1);
  v1 = 0x1a9b(varg1, v0, varg0);
  v2 = _SafeMul(v1, _protocolFeePercent);
  require(0xde0b6b3a7640000, Panic(18)); // division by zero
  v3 = _SafeMul(v1, _subjectFeePercent);
  require(0xde0b6b3a7640000, Panic(18)); // division by zero
  v4 = _SafeMul(v1, _referralFeePercent);
  require(0xde0b6b3a7640000, Panic(18)); // division by zero
  require(varg1 <= _getMyShares[varg0][msg.sender], Error("Insufficient shares"));
  require(varg1 > 0, Error("Amount must be greater than 0"));
  v5 = _SafeSub(_getMyShares[varg0][msg.sender], varg1);
  _getMyShares[varg0][msg.sender] = v5;
  v6 = _SafeSub(_sharesSupply[varg0], varg1);
  _sharesSupply[varg0] = v6;
  v7 = 0x1a9b(1, _sharesSupply[varg0], varg0);
  v8 = _SafeSub(_sharesSupply[varg0], varg1);
  v9 = _SafeSub(v1, v2 / 0xde0b6b3a7640000);
  v10 = _SafeSub(v9, v3 / 0xde0b6b3a7640000);
  v11 = _SafeSub(v10, v4 / 0xde0b6b3a7640000);
  0x30ef(v11, msg.sender);
  0x307c(v2 / 0xde0b6b3a7640000);
  0x30ef(v3 / 0xde0b6b3a7640000, varg0);
  if (v4 / 0xde0b6b3a7640000) {
    0x2f7b(v4 / 0xde0b6b3a7640000, msg.sender);
  }
  if (!_getMyShares[varg0][msg.sender]) {
    0x330f(msg.sender, varg0);
  }
  emit 0xc9d4f93ded9b42fa24561e02b2a40f720f71601eb1b3f7b3fd4eff20877639ee(
    msg.sender,
    varg0,
    bool(0),
    varg1,
    v1,
    v2 / 0xde0b6b3a7640000,
    v3 / 0xde0b6b3a7640000,
    v4 / 0xde0b6b3a7640000,
    v8,
    v7,
    _getMyShares[varg0][msg.sender]
  );
}

Step 9:

This reentrancy was abused to update the weight when the share or ticket is issued so that 1 share can be sold at a much higher price of 274,000 AVAX, thus letting the attacker earn a large profit.

undefined

Aftermath#

The team laid out its first communication, stating that there has been a major security breach with the smart contract and urging users to avoid depositing any funds while the team was actively checking the issue. Roughly four hours after this, they shared another communication stating that their site is currently experiencing DDOS attacks.

The attack turned out to be a catastrophic incident, as the protocol was drained of its entire TVL, worth almost $3 million. The founder and CEO of AvaLabs apparently enticed the community with remarks stating the loss was only three million dollars for a team that has their revenue growing exponentially.

Further communication by them ensured that they had secured the resources to close the gap caused by the exploit, and a special white-hat development team would come into play to rapidly review the security of their platform. They are likely to re-open the contract with all the funds in full after a full security audit. At the time of this writing, their platform is down for maintenance.

Solution#

Following the severe exploit on Stars Arena, which saw almost $3 million worth of funds compromised, it becomes ever more evident that we need enhanced security mechanisms and safety nets, such as those provided by Neptune Mutual. The core vulnerability that led to this exploit was a reentrancy issue in the smart contract.

Reentrancy attacks have been at the heart of numerous smart contract vulnerabilities in the distant past. One of the most recommended approaches to preventing such attacks is the Checks-Effects-Interactions (CEI) pattern. This pattern advises developers to conduct all the external interactions or calls at the very end of the function, ensuring that no state-changing operations can occur after an external call. Furthermore, introducing a mutex lock, or a binary semaphore, can act as an effective guard against reentrancy. By locking a function upon entry and unlocking it before exit, we can ensure that the function isn't re-entered before the first invocation completes, eliminating the concurrent call threat.

The lack of transparency, highlighted by the unpublished nature of Stars Arena's smart contracts, raises significant concerns. Publishing smart contracts should be a baseline requirement for any protocol that seeks to maintain trust within the decentralized finance space. Alongside this, external audits should be mandated. While an audit doesn't necessarily mean a project is entirely secure, it at least shows a project's commitment to ensuring the robustness of their code and accountability to their user base. If a project fails to publish its smart contracts and secure external audits, users should be wary and often assume potential foul play.

Additionally, forking projects like friend.tech, as Stars Arena did, can be a double-edged sword. While it offers the advantage of building upon existing codebases, any inherited vulnerabilities or issues within the original protocol can be carried over. Furthermore, any modifications made after the fork can inadvertently introduce new vulnerabilities. Developers and teams need to exercise extreme caution, ensuring they understand every line of code they inherit and rigorously test any modifications they make.

As always, potential investors and users of any platform, be it Stars Arena or any other, should perform thorough due diligence before committing their funds. The decentralized nature of the blockchain world offers many freedoms, but with that comes increased responsibility for individual users. Relying solely on project hype, marketing, or external endorsements without personal research can be a recipe for disaster.

However, even with rigorous security measures, the risk of exploitation can never be completely nullified. This inevitable uncertainty underscores the crucial role of having robust cover policies like the ones we provide at Neptune Mutual. If the team associated with Stars Arena had set up a dedicated coverage pool with us, the aftermath of the attack would have been considerably more manageable. Our policies serve as a beacon of reassurance, offering users a mitigation strategy against the loss of assets due to unforeseen vulnerabilities in smart contracts through our specialized parametric-based covers.

With us, users are not burdened with the task of providing extensive evidence of their losses to claim payouts. Once an incident is confirmed and resolved through our incident resolution system, we ensure that payouts are expedited, offering immediate relief to those affected.

Spanning multiple blockchain platforms such as EthereumArbitrum, and the BNB chain, Neptune Mutual aspires to offer its protective umbrella to the broadest spectrum of DeFi enthusiasts. Our commitment to user security not only instills a deeper sense of trust in the DeFi world but also fosters confidence, especially following major security incidents like the one at Stars Arena Global.

Reference Source Beosin

By

Tags