📜 ⬆️ ⬇️

Checking the integrity of the roulette game on the Ethereum smart contract



Few have now heard about cryptocurrencies and, in particular, Bitcoin. In 2014, on the wave of interest in Bitcoin, a new cryptocurrency appeared - Ethereum. Today, in 2017, it is the second largest by capitalization after Bitcoin. One of its most important differences from bitcoin is the use of a turing-complete virtual machine - EVM. More information about the broadcast can be found in his Yellow Paper .

Ethereum smart contracts are usually written in the Solidity language. On Habré there were already articles about writing and testing smart contracts, for example 1 , 2 , 3 . And about the connection of a smart contract with the site, you can read, for example, an article about creating the simplest voting ballot on a smart contract . This article uses the browser built into the Mist wallet, but the same can be done using a Chrome plugin, such as MetaMask . This is exactly how, through MetaMask, the game that we will explore works.

The game is a realization of European roulette : there are 37 cells in the field, numbered from 0 to 36. You can bet on a specific number or on a set of numbers: even / odd, red / black, 1-12, 1-18, etc. In each round, you can make several bets by adding a token (costing 0.01 ETH ≈ $ 0.5) to the corresponding field of the game table. Each field corresponds to the odds. For example, the rate of "red" corresponds to the coefficient 2 - that is, paying 0.01 ETH you, in case of winning, get 0.02 ETH. And if you bet on zero, the coefficient will be 36: if you pay the same 0.01 ETH for the bet, you will receive 0.36 if you win.
')
Developers, however, use a different designation: 35: 1
In the contract code, the coefficient for this bet is also indicated as 35, and the amount of the bet is added to the amount won before payment. It is possible that such a designation has been adopted in the game world, but it is more logical for me to immediately use 36.

When all bets are made, the player presses the “Play” button and, via MetaMask, sends a wager * to the Ethereum-blockchain, to the address of the smart contract of the game. The contract determines the dropped out number, calculates the results of the bets and, if necessary, sends the winnings to the player.
* - I will use the wager term to refer to a set of bets (i.e., a bet type pair - the number of bet tokens) that a player makes in one round. If you know a more correct term, please write me, I will correct.

In order to understand whether the game works honestly (that is, whether the casino does not manipulate the definition of the number dropped in its favor) let us analyze the operation of the smart contract.

His address is listed on the site of the game. In addition, before confirming payment, you can check to which address the wager will be sent. I will analyze the contract at 0xDfC328c19C8De45ac0117f836646378c10e0CdA3 . Etherscan shows its code, and for easy viewing you can use the Solidity Browser .

A contract starts with a call to the placeBet () function:

Bed sheet code
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 newcomers to Solidity, I’ll explain that the public and payable modifiers mean that the function is part of the contract API and that when you call it, you can send air. At the same time information about the sender and the amount of the broadcast sent will be available through the variable msg.

The call parameters are the bit mask of the bet types and two 32-byte arrays with the number of tokens for each of the types. You can guess this by looking at the definition of the GameInfo type and the functions getBetValueByGamble () , getBetValue () .

Another code sheet, smaller
 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; } 

Note that getBetValue () returns the amount of the bet, not in tokens, but in wei.

First of all, placeBet () checks that the contract is not turned off and then the betting checks begin.
The gambles array is the repository of all wagers played in this contract. placeBet () finds all the bets in its block and checks whether the player has sent another bet in this block and whether the allowed number of bets per block has been exceeded. Then limits on the minimum and maximum amount of the bet are checked.

In case of any error, the execution of the contract is interrupted by the throw command, which rolls back the transaction, returning the broadcast to the player.

Further, the parameters passed to the function are stored in the GameInfo structure. Here 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 air sent, RLT tokens are distributed, the referral program is processed, the bet information is saved in gambles and the PlayerBet event is created with the number and the bet amount, which is then visible in the game's web part.

About tokens
At each bet, the player is given a certain amount of RLT, Ethereum-tokens, which determine the right of the owner of the tokens to receive dividends from the profits received by the authors of the game. Read more about this - read White Paper.

Further betting life begins with a call to the ProcessGames () function, which, after the appearance of a new bid, is executed, at present, from the address 0xa92d36dc1ca4f505f1886503a0626c4aa8106497 . Such calls are well visible when viewing the list of transactions of the contract of the game : 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 is transmitted with betting numbers that need to be calculated, and ProcessGame is called for each.

 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 number of the bet and the number of blocks that must pass between the bet and its processing. When calling from ProcessGames () or ProcessGameExt (), this parameter is currently equal to 1, you can find out this value from the result of the getSettings () call.

If the block number in which processing takes place is more than 255 blocks away from the bet block, it cannot be processed: the block hash is available only for the last 256 blocks , and it is needed to determine the dropped out number.

Next, it checks whether the result of the game has already been calculated (remember, wheelResult was initialized by the number 37, which cannot fall?) And the required number of blocks have passed.

If the conditions are met, getRandomNumber () is called to determine the number that has fallen out, and the getGameResult () call calculates the win. If it is not null, the broadcast is sent to the player: g.player.send (playerWinnings) . Then an EndGame event is created that can be read from the game's web part.

Let's look at the most interesting, how the fallen out number is determined: the getRandomNumber () function.

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

Her arguments are the address of the player and the block number in which the bet was made. First of all, the function receives a hash of the block, which is separated from the block of the bet on BlockDelay blocks into the future.

This is an important point, because if a player can somehow find out the hash of this block in advance, he can form a bet that is guaranteed to win. If we recall that there are Uncle-blocks in Ethereum, there may be a problem and further analysis is required.

Next, the SHA-3 is calculated from the gluing of the player’s address and the resulting block hash. The number dropped out 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 advantage over the player.
Why do casinos bring owners profit?
On the field of 37 cells. Suppose I want to make 100,000 bets, one token for one specific field.
Probably about 2703 times I win, and all the other times I lose. In this case, for winnings from the casino, I will receive 2703 * 36 = 97 308 tokens. And 2692 tokens spent by me on bets will remain at the casino.
Similar calculations can be made for all other types of bets.


It is also interesting to see how the win is calculated. As we have seen, this is what the getGameResult () function does .

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

The parameter here is the GameInfo structure with data about the bet being calculated. And its wheelResult field is already filled in with the number drawn.

We see a cycle for all types of bets, in which the bit mask of game.bets is checked and if the bit of the type being checked is set, then winMatrix.getCoeff () is requested. winMatrix is a contract at 0x073D6621E9150bFf9d1D450caAd3c790b6F071F2 , loaded in the SmartRoulettee () constructor.

As a parameter of this function, a combination of the type of the bet and the number drawn is passed:

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

I will leave you the analysis of the WinMatrix contract code as homework, but there is nothing unexpected there: a matrix of coefficients is generated and the required one is returned when you call getCoeff () . If desired, it is easy to check it manually by calling this function on the contract page .

Source: https://habr.com/ru/post/325988/


All Articles