Analysis of the Stars Arena Exploit
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.
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.
This article was originally published by Pukar Acharya elsewhere.
Enjoy Reading This Article?
Here are some more articles you might like to read next: