How Was Galaxy Fox Token Exploited?
TL;DR
On May 10, 2024, the Galaxy Fox token was exploited on the Ethereum Mainnet due to a smart contract vulnerability, which resulted in a loss of over 108 ETH, worth approximately $330,000.
Introduction to Galaxy Fox
Galaxy Fox is a web3 platform that features their native token, captivating NFTs, a staking platform, and thrilling play-to-earn gaming ecosystem.
Vulnerability Assessment
The root cause of the exploit is a lack of regulated access control.
Steps
Step 1:
We attempt to analyse the attack transaction executed by the exploiter.
Step 2:
The vulnerable and exploited contract was unverified, so we attempted to decompile it. The setMerkleRoot function was set to a public visibility access specifier and lacked enough access control to allow anyone to change the merkel roots.
function setMerkleRoot(bytes32 _merkleRoot) public payable {
require(msg.data.length - 4 >= 32);
_merkleRoot = _merkleRoot;
}
Step 3:
This allowed them to invoke a call to the claim function to falsely claim and withdraw 108 ETH worth of funds from the contract.
function claim(address to, uint256 amount, bytes32[] proof) public payable {
require(msg.data.length - 4 >= 96);
require(proof <= uint64.max);
require(4 + proof + 31 < msg.data.length);
require(proof.length <= uint64.max);
require(4 + proof + (proof.length << 5) + 32 <= msg.data.length);
require(_claimStart > 0, Error("GfoxClaim: Not started"));
require(block.timestamp >= _claimStart, Error("GfoxClaim: Not started"));
require(amount > _claimedAmount[to], Error("GfoxClaim: Already claimed"));
v0 = new uint256[](proof.length);
CALLDATACOPY(v0.data, proof.data, proof.length << 5);
v0[proof.length] = 0;
v1 = v2 = keccak256(bytes20(to << 96), amount);
v3 = v4 = 0;
while (v3 < v0.length) {
require(v3 < v0.length, Panic(50)); // access an out-of-bounds or negative index of bytesN array or slice
v1 = v5 = 0x9c0(v0[v3], v1);
v3 += 1;
}
require(v1 == _merkleRoot, Error("GfoxClaim: Invalid proof"));
v6 = 0x718(amount);
v7 = _SafeSub(v6, _claimedAmount[to]);
v8 = _SafeAdd(_claimedAmount[to], v7);
_claimedAmount[to] = v8;
emit Claimed(to, v7, amount);
0x7d1(v7, to, address(0x8f1cece048cade6b8a05dfa2f90ee4025f4f2662));
}
The flashbot private transactions sent by the actual attacker were front-run by the original attacker, who was able to steal the assets.
Step 4:
The original attacker was followed by yet another copycat attacker, who was also able to successfully execute the said attack to take away the remaining funds. The original attacker approved 99.79 ETH or $289,221 for his trade and has a hold of 9.03 ETH at this address, which is worth roughly $26,167.
Step 5:
The copycat attacker took away approximately 27.8 million GFOX tokens and swapped them for 2.32 ETH, which were worth roughly $7029. As of this writing, the wallet of the copycat attacker has a hold of 2.17 ETH, which is approximately $6,294. This attacker has also already laundered 2 ETH, amounting to roughly $5,800 to Tornado Cash, as seen in these and other transactions.
Solution
To address the vulnerability exploited in the Galaxy Fox token incident, a comprehensive revision of the smart contract’s access control mechanisms is essential. The primary flaw was the public accessibility of the setMerkleRoot function, which should have been restricted to only authorized addresses. Implementing proper access control can be effectively achieved by utilizing modifiers in the Solidity programming language.
One effective solution is to introduce a modifier that checks whether the message sender, or msg.sender
, is an authorized user. This can be done by maintaining a list of authorized addresses of the owner in a mapping. The owner could be a state variable set to the deployer’s address, or alternatively, it could be a role-based system where multiple addresses are authorized to perform certain actions. This modifier is then applied to the setMerkleRoot
function, ensuring that only an authorized user can execute it.
Beyond simple owner-based controls, a more robust and flexible access control system can be implemented using role-based access control (RBAC). This system can manage different levels of permissions for different types of actions within the contract. The OpenZeppelin library provides well-tested contracts for such purposes, including AccessControl, which allows for multiple roles to be created and managed. Each role can be assigned specific permissions, and multiple accounts can be granted or revoked roles dynamically.
Implementing these changes would significantly enhance the security of the contract by restricting critical functionalities to only those addresses that are explicitly authorized. Moreover, it is crucial to undergo thorough audits and extensive testing on testnets before deploying the contract on the mainnet. These audits should be conducted by independent security experts to ensure that no vulnerabilities remain.
This article was originally published by Pukar Acharya elsewhere.
Enjoy Reading This Article?
Here are some more articles you might like to read next: