How fair is the roulette on the Ethereum smart contract? An independent audit of the game contract
We present to your attention a roulette audit from an independent developer Paul Rubin. Read, study and share your opinion about roulette!
Today it is very unlikely to meet a person who hasn't heard about cryptocurrencies and, in particular, about Bitcoin. In 2014, on a wave of interest in bitcoin, a new cryptocurrency - Ethereum, has been created. Today, in 2018, it has the largest capitalization after bitcoin. One of its most important differences from bitcoin is the use of a Turing virtual machine - EVM. More information about the Ethererum can be found in its Yellow Paper.
Today we will explore a game that works with Metamask wallet, a plugin for Google Chrome.
The game is the realization of the European roulette: there is the field with 37 cells numbered from 0 to 36. A player can bet on a specific number, or on a set of numbers: even/odd, red/black, 1-12, 1-18, etc. In each round, the player can make multiple bets (the minimum bet size is 0.01 ETH ≈ $ 7.26) on the corresponding field of the game table. Each field corresponds to a winning ratio. For example, the rate "to red" corresponds to a coefficient of 2 - placing a bet in amount of 0.01 ETH, in case of winning, the player will receive 0.02 ETH. And if the player bets on zero, the coefficient will be 36: paying the same 0.01 ETH for the bet the player will get 0.36.
Note
In the game contract, the coefficient for this bet is also listed as 35, and the bet amount is added to the amount of the win before the payment.
When all bets are made, the player presses the "Play" button and, via MetaMask, sends a bet to the Ethereum blockchain to the roulette contract address. The contract determines the random number, calculates the results of the bets and, if necessary, sends the winnings to the player.
In order to understand honestly whether the game is working (that is, if the casino does not manipulate when determining the random number for mercenary purposes), the work of the smart contract should be analyzed.
Its address is listed on the game website. In addition, before confirming the payment, you can check where the bet will be sent. The contract at the address 0xDfC328c19C8De45ac0117f836646378c10e0CdA3 will be analyzed as an example. Etherscan shows its code, and for convenient viewing, you can use Solidity Browser. The work of the contract begins with the placeBet() function:function placeBet(uint256 bets, bytes32 values1,bytes32 values2) public payable
{
if (ContractState == false)
{
ErrorLog(msg.sender, "ContractDisabled");
if (msg.sender.send(msg.value) == false) throw;
return;
}
var gamblesLength = gambles.length;
if (gamblesLength > 0)
{
uint8 gamblesCountInCurrentBlock = 0;
for(var i = gamblesLength - 1;i > 0; i--)
{
if (gambles[i].blockNumber == block.number)
{
if (gambles[i].player == msg.sender)
{
ErrorLog(msg.sender, "Play twice the same block");
if (msg.sender.send(msg.value) == false) throw;
return;
}
gamblesCountInCurrentBlock++;
if (gamblesCountInCurrentBlock >= maxGamblesPerBlock)
{
ErrorLog(msg.sender, "maxGamblesPerBlock");
if (msg.sender.send(msg.value) == false) throw;
return;
}
}
else
{
break;
}
}
}
var _currentMaxBet = currentMaxBet;
if (msg.value < _currentMaxBet/256 || bets == 0)
{
ErrorLog(msg.sender, "Wrong bet value");
if (msg.sender.send(msg.value) == false) throw;
return;
}
if (msg.value > _currentMaxBet)
{
ErrorLog(msg.sender, "Limit for table");
if (msg.sender.send(msg.value) == false) throw;
return;
}
GameInfo memory g = GameInfo(msg.sender, block.number, 37, bets, values1,values2);
if (totalBetValue(g) != msg.value)
{
ErrorLog(msg.sender, "Wrong bet value");
if (msg.sender.send(msg.value) == false) throw;
return;
}
address affiliate = 0;
uint16 coef_affiliate = 0;
uint16 coef_player;
if (address(smartAffiliateContract) > 0)
{
(affiliate, coef_affiliate, coef_player) = smartAffiliateContract.getAffiliateInfo(msg.sender);
}
else
{
coef_player = CoefPlayerEmission;
}
uint256 playerTokens;
uint8 errorCodeEmission;
(playerTokens, errorCodeEmission) = smartToken.emission(msg.sender, affiliate, msg.value, coef_player, coef_affiliate);
if (errorCodeEmission != 0)
{
if (errorCodeEmission == 1)
ErrorLog(msg.sender, "token operations stopped");
else if (errorCodeEmission == 2)
ErrorLog(msg.sender, "contract is not in a games list");
else if (errorCodeEmission == 3)
ErrorLog(msg.sender, "incorect player address");
else if (errorCodeEmission == 4)
ErrorLog(msg.sender, "incorect value bet");
else if (errorCodeEmission == 5)
ErrorLog(msg.sender, "incorect Coefficient emissions");
if (msg.sender.send(msg.value) == false) throw;
return;
}
gambles.push(g);
PlayerBet(gamblesLength, playerTokens);
}
For Solidity newbies: the public and payable modifiers mean that the function is part of the API contract and when you call it, you can send ETH. In this case, information about the sender and the ETH amount will be available via the variable msg. The call parameters are the bit mask of bet types and two 32-byte arrays with the number of bets per type. You can guess this by looking at the definition of the GameInfo type and the functions getBetValueByGamble(), getBetValue ().
struct GameInfo
{
address player;
uint256 blockNumber;
uint8 wheelResult;
uint256 bets;
bytes32 values;
bytes32 values2;
}
// n - number player bet
// nBit - betIndex
function getBetValueByGamble(GameInfo memory gamble, uint8 n, uint8 nBit) private constant returns (uint256)
{
if (n <= 32) return getBetValue(gamble.values , n, nBit);
if (n <= 64) return getBetValue(gamble.values2, n - 32, nBit);
// there are 64 maximum unique bets (positions) in one game
throw;
}
// n form 1 <= to <= 32
function getBetValue(bytes32 values, uint8 n, uint8 nBit) private constant returns (uint256)
{
// bet in credits (1..256)
uint256 bet = uint256(values[32 - n]) + 1;
if (bet < uint256(minCreditsOnBet[nBit]+1)) throw; //default: bet < 0+1
if (bet > uint256(256-maxCreditsOnBet[nBit])) throw; //default: bet > 256-0
return currentMaxBet * bet / 256;
}
It is worth noting that getBetValue() returns the amount of the bet in wei.
The first thing placeBet() checks is that the contract is not turned off and then bet checks begin.
The gambles array is the repository of all the bets placed in this contract. placeBet() finds all the bets in its block and checks whether the current player has sent another bet in this block and whether the allowed amount of betting in the block is exceeded. Then, restrictions on the minimum and maximum amount of the bet are checked.
In the event of any error, the execution of the contract is terminated by the throw command, which rolls back the transaction, returning ETH to the player.
Further, the parameters passed to the function are stored in the GameInfo structure. It is important for us that the wheelResult field is initialized with the number 37.
After another check that the amount of bets coincides with the amount of ETH sent, the RLT tokens are distributed, the referral program is processed, the bet information is stored in gambles and a PlayerBet event is generated with the number and amount of the bet, which is seen in the game's web part.
RLT tokens_At each bet, the player is given a certain number of RLT, ERC-20 tokens, which determine the right of the owner of tokens to receive the loyalty rewards from the profits received by the game creators. More information about this is given in
White Paper.
The further life of the bet begins with calling the function ProcessGames(), which, after the appearance of a new bet, is being executed, at the present time, from the address 0xa92d36dc1ca4f505f1886503a0626c4aa8106497. Such calls are well visible when viewing the list of transactions of the game contract: they have Value=0.
ProcessGames code
function ProcessGames(uint256[] gameIndexes, bool simulate)
{
if (!simulate)
{
if (lastBlockGamesProcessed == block.number) return;
lastBlockGamesProcessed = block.number;
}
uint8 delay = BlockDelay;
uint256 length = gameIndexes.length;
bool success = false;
for(uint256 i = 0;i < length;i++)
{
if (ProcessGame(gameIndexes[i], delay) == GameStatus.Success) success = true;
}
if (simulate && !success) throw;
}
In the call parameters, an array with the bet numbers requiring calculation is transmitted, and for each ProcessGame is called.
function ProcessGame(uint256 index, uint256 delay) private returns (GameStatus)
{
GameInfo memory g = gambles[index];
if (block.number - g.blockNumber >= 256) return GameStatus.Stop;
if (g.wheelResult == 37 && block.number > g.blockNumber + delay)
{
gambles[index].wheelResult = getRandomNumber(g.player, g.blockNumber);
uint256 playerWinnings = getGameResult(gambles[index]);
if (playerWinnings > 0)
{
if (g.player.send(playerWinnings) == false) throw;
}
EndGame(g.player, gambles[index].wheelResult, index);
return GameStatus.Success;
}
return GameStatus.Skipped;
}
The call parameters are the bet number and the number of blocks that must pass between the bet and its processing. When called from ProcessGames() or ProcessGameExt(), this parameter is currently 1, this value can be learned from the result of calling getSettings().
In the event that the number of the block in which processing takes place is more than 255 blocks apart from the betting block, it can not be processed: the block hash is available only for the last 256 blocks, and it is needed to determine the number dropped.
Next, the code checks whether the result of the game has already been calculated (remember, wheelResult was initialized with the number 37, which can't be the game result?) and whether the required number of blocks has already passed.
If the conditions are met, the call to getRandomNumber() is done to determine the random number, the win is calculated by calling getGameResult(). If it is not zero, ETH is sent to the player: g.player.send(playerWinnings). Then an EndGame event is created, which can be read from the web part of the game.
Let's see the most interesting part: how to determine the random number: the function getRandomNumber().
function getRandomNumber(address player, uint256 playerblock) private returns(uint8 wheelResult)
{
// block.blockhash - hash of the given block - only works for 256 most recent blocks excluding current
bytes32 blockHash = block.blockhash(playerblock+BlockDelay);
if (blockHash==0)
{
ErrorLog(msg.sender, "Cannot generate random number");
wheelResult = 200;
}
else
{
bytes32 shaPlayer = sha3(player, blockHash);
wheelResult = uint8(uint256(shaPlayer)%37);
}
}
Its arguments are the player's address and the block number in which the bet was placed. The first thing the function gets is a block hash, which is separated from the bet block for BlockDelay blocks into the future.
This is an important point because if a player can somehow learn the hash of this block in advance, he can form a bet that is guaranteed to win. If you remember that there are Uncle blocks in Ethereum, there may be a problem and further analysis is required.
Next, SHA-3 is calculated from the gluing of the player's address and the received block hash. The dropped number is calculated by taking the remainder of dividing the result of SHA-3 by 37.
From my point of view, the algorithm is quite honest and the casino has no precedence over the player.Why does the casino bring profit to its owners?There are 37 cells on the field. For example, a player wants to make 100 000 bets on one particular field. Probably, about 2703 times the player will win, and all the other times he will lose. In this case, from the casino winnings, the player will receive 2703 * 36 = 97,308 RLT tokens. And 2692 RLT tokens, spent on bets, will go to the casino. Similar calculations can be made for all other types of bets.
It is also interesting to see, how the win is calculated. The getGameResult() function does this:
function getGameResult(GameInfo memory game) private constant returns (uint256 totalWin)
{
totalWin = 0;
uint8 nPlayerBetNo = 0;
// we sent count bets at last byte
uint8 betsCount = uint8(bytes32(game.bets)[0]);
for(uint8 i=0; i {
if (isBitSet(game.bets, i))
{
var winMul = winMatrix.getCoeff(getIndex(i, game.wheelResult)); // get win coef
if (winMul > 0) winMul++; // + return player bet
totalWin += winMul * getBetValueByGamble(game, nPlayerBetNo+1,i);
nPlayerBetNo++;
if (betsCount == 1) break;
betsCount--;
}
}
}
Parameter here is the structure of GameInfo with data about the calculated bet. And its wheelResult field is already filled with a random number.
Here is the cycle for all types of bets, in which the bit mask game.bets is checked and if the bit of the type being checked is installed, winMatrix.getCoeff() is requested. winMatrix is the contract with the address 0x073D6621E9150bFf9d1D450caAd3c790b6F071F2, loaded in the SmartRoulettee() constructor.
A parameter of this function is a combination of the bet type and the random number:
// unique combination of bet and wheelResult, used for access to WinMatrix
function getIndex(uint16 bet, uint16 wheelResult) private constant returns (uint16)
{
return (bet+1)*256 + (wheelResult+1);
}
Have you played the Blockchain roulette by SmartPlay.tech? Don’t forget to share your opinion with other players, join the Telegram chat with the early project investors and ask you questions to the official representatives.