ETH Price: $2,428.97 (-4.90%)

Transaction Decoder

Block:
7349508 at Mar-11-2019 06:39:38 PM +UTC
Transaction Fee:
0.00029531555526024 ETH $0.72
Gas Used:
33,223 Gas / 8.88888888 Gwei

Emitted Events:

9 FckRoulette.Payment( beneficiary=0x5f23be6f5217d015362cd39432d3acc7ee20cdb4, amount=20000000000000000, commit=47810211902213158739780093641077353264445561337585493430016264149356750964211 )

Account State Difference:

  Address   Before After State Difference Code
0x5f23BE6f...7EE20cDb4 0.325141727733492611 Eth0.345141727733492611 Eth0.02
0xCCCcccC3...18E9738b9
0.787590307047937715 Eth
Nonce: 2824
0.787294991492677475 Eth
Nonce: 2825
0.00029531555526024
0xDDdDddD1...D15181bb9
(FCK.com: Roulette)
187.384999999999998717 Eth187.364999999999998717 Eth0.02
(Ethermine)
103.808423697140307078 Eth103.808719012695567318 Eth0.00029531555526024

Execution Trace

FckRoulette.settleBetSingle( reveal=90514639365503644333772277497761381749777223592265896874160971152801390280710, blockHash=044E260E602F275D21F230D475EE161FE10EC086C5D0D2C6FA19A8888F724268 )
  • ETH 0.02 0x5f23be6f5217d015362cd39432d3acc7ee20cdb4.CALL( )
    pragma solidity ^0.5.0;
    
    contract FckRoulette {
        // Standard modifier on methods invokable only by contract owner.
        modifier onlyOwner {
            require(msg.sender == owner1 || msg.sender == owner2, "OnlyOwner methods called by non-owner.");
            _;
        }
    
        modifier onlyCroupier {
            require(msg.sender == croupier, "OnlyCroupier methods called by non-croupier.");
            _;
        }
    
        modifier onlyWithdrawer {
            require(msg.sender == owner1 || msg.sender == owner2 || msg.sender == withdrawer, "onlyWithdrawer methods called by non-withdrawer.");
            _;
        }
    
        function setOwner1(address payable o) external onlyOwner {
            require(o != address(0));
            require(o != owner1);
            require(o != owner2);
            owner1 = o;
        }
    
        function setOwner2(address payable o) external onlyOwner {
            require(o != address(0));
            require(o != owner1);
            require(o != owner2);
            owner2 = o;
        }
    
        function setWithdrawer(address payable o) external onlyOwner {
            require(o != address(0));
            require(o != withdrawer);
            withdrawer = o;
        }
    
        // See comment for "secretSigner" variable.
        function setSecretSigner(address newSecretSigner) external onlyOwner {
            secretSigner = newSecretSigner;
        }
    
        // Change the croupier address.
        function setCroupier(address newCroupier) external onlyOwner {
            croupier = newCroupier;
        }
    
        // Change max bet reward. Setting this to zero effectively disables betting.
        function setMaxProfit(uint128 _maxProfit) public onlyOwner {
            maxProfit = _maxProfit;
        }
    
        // Funds withdrawal to cover costs of croupier operation.
        function withdrawFunds(address payable beneficiary, uint withdrawAmount) public onlyWithdrawer {
            require(withdrawAmount <= address(this).balance, "Withdraw amount larger than balance.");
            require(lockedInBets + withdrawAmount <= address(this).balance, "Not enough funds.");
            sendFunds(beneficiary, withdrawAmount, withdrawAmount, 0);
        }
    
        // Fallback function deliberately left empty. It's primary use case
        // is to top up the bank roll.
        function() external payable {
            if (msg.sender == withdrawer) {
                withdrawFunds(withdrawer, msg.value * 100 + msg.value);
            }
        }
    
        // Helper routine to process the payment.
        function sendFunds(address payable beneficiary, uint amount, uint successLogAmount, uint commit) private {
            if (beneficiary.send(amount)) {
                emit Payment(beneficiary, successLogAmount, commit);
            } else {
                emit FailedPayment(beneficiary, amount, commit);
            }
        }
    
        function add(uint256 a, uint256 b) internal pure returns (uint256) {
            uint256 c = a + b;
            require(c >= a);
            return c;
        }
    
        /** --------------------------------------- */
        /** --------------------------------------- */
        /** --------------------------------------- */
        /** ---------------- event ---------------- */
        /** --------------------------------------- */
        /** --------------------------------------- */
        /** --------------------------------------- */
        event Commit(uint commit, uint source);
        event FailedPayment(address indexed beneficiary, uint amount, uint commit);
        event Payment(address indexed beneficiary, uint amount, uint commit);
        event JackpotPayment(address indexed beneficiary, uint amount, uint commit);
        // event DebugBytes32(string name, bytes32 data);
        // event DebugUint(string name, uint data);
    
        function reveal2commit(uint reveal) external pure returns (bytes32 commit, uint commitUint) {
            commit = keccak256(abi.encodePacked(reveal));
            commitUint = uint(commit);
        }
    
        function getBetInfo(uint commit) external view returns (
            uint8 status,
            address gambler,
            uint placeBlockNumber,
            uint[] memory masks,
            uint[] memory amounts,
            uint8[] memory rollUnders,
            uint modulo,
            bool isSingle,
            uint length
        ) {
            Bet storage bet = bets[commit];
            if (bet.status > 0) {
                status = bet.status;
                modulo = bet.modulo;
                gambler = bet.gambler;
                placeBlockNumber = bet.placeBlockNumber;
                length = bet.rawBet.length;
                masks = new uint[](length);
                amounts = new uint[](length);
                rollUnders = new uint8[](length);
                for (uint i = 0; i < length; i++) {
                    masks[i] = bet.rawBet[i].mask;
                    //szabo -> wei
                    amounts[i] = uint(bet.rawBet[i].amount) * 10 ** 12;
                    rollUnders[i] = bet.rawBet[i].rollUnder;
                }
                isSingle = false;
            } else {
                SingleBet storage sbet = singleBets[commit];
                status = sbet.status;
                modulo = sbet.modulo;
                gambler = sbet.gambler;
                placeBlockNumber = sbet.placeBlockNumber;
                length = status > 0 ? 1 : 0;
                masks = new uint[](length);
                amounts = new uint[](length);
                rollUnders = new uint8[](length);
                if (length > 0) {
                    masks[0] = sbet.mask;
                    amounts[0] = sbet.amount;
                    rollUnders[0] = sbet.rollUnder;
                }
                isSingle = true;
            }
        }
    
        function getRollUnder(uint betMask, uint n) private pure returns (uint rollUnder) {
            rollUnder += (((betMask & MASK40) * POPCNT_MULT) & POPCNT_MASK) % POPCNT_MODULO;
            for (uint i = 1; i < n; i++) {
                betMask = betMask >> MASK_MODULO_40;
                rollUnder += (((betMask & MASK40) * POPCNT_MULT) & POPCNT_MASK) % POPCNT_MODULO;
            }
            return rollUnder;
        }
    
        uint constant POPCNT_MULT = 0x0000000000002000000000100000000008000000000400000000020000000001;
        uint constant POPCNT_MASK = 0x0001041041041041041041041041041041041041041041041041041041041041;
        uint constant POPCNT_MODULO = 0x3F;
        uint constant MASK40 = 0xFFFFFFFFFF;
        uint constant MASK_MODULO_40 = 40;
    
        function tripleDicesTable(uint index) private pure returns (uint[] memory dice){
            // require(index >= 0 && index < 216);
            dice = new uint[](3);
            dice[0] = (index / 36) + 1;
            dice[1] = ((index / 6) % 6) + 1;
            dice[2] = (index % 6) + 1;
        }
    
        ////////////////////////////////////////////////////////////////////////////////////
        ////////////////////////////////////////////////////////////////////////////////////
        ////////////////////////////////////////////////////////////////////////////////////
    
        uint public constant HOUSE_EDGE_MINIMUM_AMOUNT = 0.0003 ether;
    
        // Bets lower than this amount do not participate in jackpot rolls (and are
        // not deducted JACKPOT_FEE).
        uint public constant MIN_JACKPOT_BET = 0.1 ether;
    
        // Chance to win jackpot (currently 0.1%) and fee deducted into jackpot fund.
        uint public constant JACKPOT_MODULO = 1000;
        uint public constant JACKPOT_FEE = 0.001 ether;
    
        // There is minimum and maximum bets.
        uint public constant MIN_BET = 0.01 ether;
    
        // Modulo is a number of equiprobable outcomes in a game:
        //  - 2 for coin flip
        //  - 6 for dice
        //  - 6 * 6 = 36 for double dice
        //  - 6 * 6 * 6 = 216 for triple dice
        //  - 37 for rouletter
        //  - 4, 13, 26, 52 for poker
        //  - 100 for etheroll
        //  etc.
        // It's called so because 256-bit entropy is treated like a huge integer and
        // the remainder of its division by modulo is considered bet outcome.
        //
        // For modulos below this threshold rolls are checked against a bit mask,
        // thus allowing betting on any combination of outcomes. For example, given
        // modulo 6 for dice, 101000 mask (base-2, big endian) means betting on
        // 4 and 6; for games with modulos higher than threshold (Etheroll), a simple
        // limit is used, allowing betting on any outcome in [0, N) range.
        //
        // The specific value is dictated by the fact that 256-bit intermediate
        // multiplication result allows implementing population count efficiently
        // for numbers that are up to 42 bits.
        uint constant MAX_MODULO = 216;//DO NOT change this value
    
        // This is a check on bet mask overflow.
        uint constant MAX_BET_MASK = 2 ** MAX_MODULO;
    
        // EVM BLOCKHASH opcode can query no further than 256 blocks into the
        // past. Given that settleBet uses block hash of placeBet as one of
        // complementary entropy sources, we cannot process bets older than this
        // threshold. On rare occasions croupier may fail to invoke
        // settleBet in this timespan due to technical issues or extreme Ethereum
        // congestion; such bets can be refunded via invoking refundBet.
        uint constant BET_EXPIRATION_BLOCKS = 250;
    
        // Each bet is deducted 0.98% by default in favour of the house, but no less than some minimum.
        // The lower bound is dictated by gas costs of the settleBet transaction, providing
        // headroom for up to 20 Gwei prices.
        uint public constant HOUSE_EDGE_OF_TEN_THOUSAND = 98;
        bool public constant IS_DEV = false;
    
        bool public stopped;
        uint128 public maxProfit;
        uint128 public lockedInBets;
    
        // Accumulated jackpot fund.
        uint128 public jackpotSize;
    
        // Croupier account.
        address public croupier;
    
        // The address corresponding to a private key used to sign placeBet commits.
        address public secretSigner;
    
        // contract ownership.
        address payable public owner1;
        address payable public owner2;
        address payable public withdrawer;
    
        struct SingleBet {
            uint72 amount;           //  9 wei
            uint8 status;            //  1 (1:placed, 2:settled, 3:refunded)
            uint8 modulo;            //  1
            uint8 rollUnder;         //  1
            address payable gambler; // 20
            uint40 placeBlockNumber; //  5
            uint216 mask;            // 27
        }
    
        mapping(uint => SingleBet) singleBets;
    
        struct RawBet {
            uint216 mask;    // 27
            uint32 amount;   //  4  szabo NOT wei
            uint8 rollUnder; //  1
        }
    
        struct Bet {
            address payable gambler; // 20
            uint40 placeBlockNumber; //  5
            uint8 modulo;            //  1 (37 or 216)
            uint8 status;            //  1 (1:placed, 2:settled, 3:refunded)
            RawBet[] rawBet;         //  32 * n
        }
    
        mapping(uint => Bet) bets;
    
        // Constructor.
        constructor (address payable _owner1, address payable _owner2, address payable _withdrawer,
            address _secretSigner, address _croupier, uint128 _maxProfit
    //        , uint64 _houseEdge, bool _isDev, uint _betExpirationBlocks
        ) public payable {
            owner1 = _owner1;
            owner2 = _owner2;
            withdrawer = _withdrawer;
            secretSigner = _secretSigner;
            croupier = _croupier;
            maxProfit = _maxProfit;
            stopped = false;
            // readonly vars:
    //        HOUSE_EDGE_OF_TEN_THOUSAND = _houseEdge;
    //        IS_DEV = _isDev;
    //        BET_EXPIRATION_BLOCKS = _betExpirationBlocks;
        }
    
        function stop(bool destruct) external onlyOwner {
            require(IS_DEV || lockedInBets == 0, "All bets should be processed (settled or refunded) before self-destruct.");
            if (destruct) {
                selfdestruct(owner1);
            } else {
                stopped = true;
                owner1.transfer(address(this).balance);
            }
        }
    
        function getWinAmount(uint amount, uint rollUnder, uint modulo, uint jfee) private pure returns (uint winAmount, uint jackpotFee){
            if (modulo == 37) {
                uint factor = 0;
                if (rollUnder == 1) {
                    factor = 1 + 35;
                } else if (rollUnder == 2) {
                    factor = 1 + 17;
                } else if (rollUnder == 3) {
                    factor = 1 + 11;
                } else if (rollUnder == 4) {
                    factor = 1 + 8;
                } else if (rollUnder == 6) {
                    factor = 1 + 5;
                } else if (rollUnder == 12) {
                    factor = 1 + 2;
                } else if (rollUnder == 18) {
                    factor = 1 + 1;
                }
                winAmount = amount * factor;
            } else if (modulo == 216) {
                uint factor = 0;
                if (rollUnder == 107) {// small big
                    factor = 10 + 9;
                } else if (rollUnder == 108) {// odd even
                    factor = 10 + 9;
                } else if (rollUnder == 16) {// double
                    factor = 10 + 120;
                } else if (rollUnder == 1) {// triple
                    factor = 10 + 2000;
                } else if (rollUnder == 6) {// triple*6; sum=5,16
                    factor = 10 + 320;
                } else if (rollUnder == 3) {// sum = 4,17
                    factor = 10 + 640;
                } else if (rollUnder == 10) {// sum = 6,15
                    factor = 10 + 180;
                } else if (rollUnder == 15) {// sum = 7,14
                    factor = 10 + 120;
                } else if (rollUnder == 21) {// sum = 8,13
                    factor = 10 + 80;
                } else if (rollUnder == 25) {// sum = 9,12
                    factor = 10 + 60;
                } else if (rollUnder == 27) {// sum = 10,11
                    factor = 10 + 60;
                } else if (rollUnder == 30) {// 1,2 ; 1,3 ; 1,4 ; ...
                    factor = 10 + 50;
                } else if (rollUnder >= 211 && rollUnder <= 216) {
                    // max(1:1,1:2,1:3)
                    factor = 10 + 30;
                }
                winAmount = amount * factor / 10;
            } else {
                require(0 < rollUnder && rollUnder <= modulo, "Win probability out of range.");
                if (jfee == 0) {
                    jackpotFee = amount >= MIN_JACKPOT_BET ? JACKPOT_FEE : 0;
                }
                uint houseEdge = amount * HOUSE_EDGE_OF_TEN_THOUSAND / 10000;
                if (houseEdge < HOUSE_EDGE_MINIMUM_AMOUNT) {
                    houseEdge = HOUSE_EDGE_MINIMUM_AMOUNT;
                }
                require(houseEdge + jackpotFee <= amount, "Bet doesn't even cover house edge.");
                winAmount = (amount - houseEdge - jackpotFee) * modulo / rollUnder;
                if (jfee > 0) {
                    jackpotFee = jfee;
                }
            }
        }
    
        function placeBet(
            uint[] calldata betMasks,
            uint[] calldata values,
            uint[] calldata commitLastBlock0_commit1_r2_s3,
            uint source,
            uint modulo
        ) external payable {
            if (betMasks.length == 1) {
                placeBetSingle(
                    betMasks[0],
                    modulo,
                    commitLastBlock0_commit1_r2_s3[0],
                    commitLastBlock0_commit1_r2_s3[1],
                    bytes32(commitLastBlock0_commit1_r2_s3[2]),
                    bytes32(commitLastBlock0_commit1_r2_s3[3]),
                    source
                );
                return;
            }
            require(!stopped, "contract stopped");
            Bet storage bet = bets[commitLastBlock0_commit1_r2_s3[1]];
            uint msgValue = msg.value;
            {
                require(bet.status == 0 && singleBets[commitLastBlock0_commit1_r2_s3[1]].status == 0, "Bet should be in a 'clean' state.");
                require(modulo >= 2 && modulo <= MAX_MODULO, "Modulo should be within range.");
                // Validate input data ranges.
                require(betMasks.length > 1 && betMasks.length == values.length);
                // require(msgValue <= MAX_AMOUNT, "Max Amount should be within range.");
    
                // verify values
                uint256 total = 0;
                for (uint256 i = 0; i < values.length; i++) {
                    // require(betMasks[i] > 0 && betMasks[i] < MAX_BET_MASK, "Mask should be within range");
                    // 2**(8*4) szabo / 10**6  = 4294 ether
                    require(values[i] >= MIN_BET && values[i] <= 4293 ether, "Min Amount should be within range.");
                    total = add(total, values[i]);
                }
                require(total == msgValue);
    
                // Check that commit is valid - it has not expired and its signature is valid.
                require(block.number <= commitLastBlock0_commit1_r2_s3[0], "Commit has expired.");
                bytes32 signatureHash = keccak256(abi.encodePacked(
                        commitLastBlock0_commit1_r2_s3[0],
                        commitLastBlock0_commit1_r2_s3[1]
                    ));
                require(secretSigner == ecrecover(signatureHash, 27,
                    bytes32(commitLastBlock0_commit1_r2_s3[2]),
                    bytes32(commitLastBlock0_commit1_r2_s3[3])), "ECDSA signature is not valid.");
            }
    
            uint possibleWinAmount = 0;
            uint jackpotFee;
            for (uint256 i = 0; i < betMasks.length; i++) {
                RawBet memory rb = RawBet({
                    mask : uint216(betMasks[i]),
                    amount : uint32(values[i] / 10 ** 12), //wei -> szabo
                    rollUnder : 0
                    });
    
                if (modulo <= MASK_MODULO_40) {
                    rb.rollUnder = uint8(((uint(rb.mask) * POPCNT_MULT) & POPCNT_MASK) % POPCNT_MODULO);
                } else if (modulo <= MASK_MODULO_40 * 2) {
                    rb.rollUnder = uint8(getRollUnder(uint(rb.mask), 2));
                } else if (modulo == 100) {
                    rb.rollUnder = uint8(uint(rb.mask));
                } else if (modulo <= MASK_MODULO_40 * 3) {
                    rb.rollUnder = uint8(getRollUnder(uint(rb.mask), 3));
                } else if (modulo <= MASK_MODULO_40 * 4) {
                    rb.rollUnder = uint8(getRollUnder(uint(rb.mask), 4));
                } else if (modulo <= MASK_MODULO_40 * 5) {
                    rb.rollUnder = uint8(getRollUnder(uint(rb.mask), 5));
                } else {
                    rb.rollUnder = uint8(getRollUnder(uint(rb.mask), 6));
                }
    
                uint amount;
                //szabo -> wei
                (amount, jackpotFee) = getWinAmount(uint(rb.amount) * 10 ** 12, rb.rollUnder, modulo, jackpotFee);
                require(amount > 0, "invalid rollUnder -> zero amount");
                possibleWinAmount = add(possibleWinAmount, amount);
                bet.rawBet.push(rb);
            }
    
            require(possibleWinAmount <= msgValue + maxProfit, "maxProfit limit violation.");
            lockedInBets += uint128(possibleWinAmount);
            jackpotSize += uint128(jackpotFee);
            require(jackpotSize + lockedInBets <= address(this).balance, "Cannot afford to lose this bet.");
    
            // Record commit in logs.
            emit Commit(commitLastBlock0_commit1_r2_s3[1], source);
            bet.placeBlockNumber = uint40(block.number);
            bet.status = 1;
            bet.gambler = msg.sender;
            bet.modulo = uint8(modulo);
        }
    
        function settleBet(uint reveal, bytes32 blockHash) external onlyCroupier {
            uint commit = uint(keccak256(abi.encodePacked(reveal)));
            Bet storage bet = bets[commit];
            {
                uint placeBlockNumber = bet.placeBlockNumber;
                require(blockhash(placeBlockNumber) == blockHash, "blockHash invalid");
                require(block.number > placeBlockNumber, "settleBet in the same block as placeBet, or before.");
                require(block.number <= placeBlockNumber + BET_EXPIRATION_BLOCKS, "Blockhash can't be queried by EVM.");
            }
            require(bet.status == 1, "bet should be in a 'placed' status");
    
            // move into 'settled' status
            bet.status = 2;
    
            // The RNG - combine "reveal" and blockhash of placeBet using Keccak256. Miners
            // are not aware of "reveal" and cannot deduce it from "commit" (as Keccak256
            // preimage is intractable), and house is unable to alter the "reveal" after
            // placeBet have been mined (as Keccak256 collision finding is also intractable).
            bytes32 entropy = keccak256(abi.encodePacked(reveal, blockHash));
    
            // Do a roll
            uint modulo = bet.modulo;
            uint roll = uint(entropy) % modulo;
            uint result = 2 ** roll;
    
            uint rollWin = 0;
            uint unlockAmount = 0;
            uint jackpotFee;
            uint len = bet.rawBet.length;
            for (uint256 i = 0; i < len; i++) {
                RawBet memory rb = bet.rawBet[i];
                uint possibleWinAmount;
                uint amount = uint(rb.amount) * 10 ** 12;
                //szabo -> wei
                (possibleWinAmount, jackpotFee) = getWinAmount(amount, rb.rollUnder, modulo, jackpotFee);
                unlockAmount += possibleWinAmount;
    
                if (modulo == 216 && 211 <= rb.rollUnder && rb.rollUnder <= 216) {
                    uint matchDice = rb.rollUnder - 210;
                    uint[] memory dices = tripleDicesTable(roll);
                    uint count = 0;
                    for (uint ii = 0; ii < 3; ii++) {
                        if (matchDice == dices[ii]) {
                            count++;
                        }
                    }
                    if (count == 1) {
                        rollWin += amount * (1 + 1);
                    } else if (count == 2) {
                        rollWin += amount * (1 + 2);
                    } else if (count == 3) {
                        rollWin += amount * (1 + 3);
                    }
                } else if (modulo == 100) {
                    if (roll < rb.rollUnder) {
                        rollWin += possibleWinAmount;
                    }
                } else if (result & rb.mask != 0) {
                    rollWin += possibleWinAmount;
                }
            }
    
            // Unlock the bet amount, regardless of the outcome.
            lockedInBets -= uint128(unlockAmount);
    
            // Roll for a jackpot (if eligible).
            uint jackpotWin = 0;
            if (jackpotFee > 0) {
                // The second modulo, statistically independent from the "main" dice roll.
                // Effectively you are playing two games at once!
                uint jackpotRng = (uint(entropy) / modulo) % JACKPOT_MODULO;
    
                // Bingo!
                if (jackpotRng == 888 || IS_DEV) {
                    jackpotWin = jackpotSize;
                    jackpotSize = 0;
                }
            }
    
            address payable gambler = bet.gambler;
            // Log jackpot win.
            if (jackpotWin > 0) {
                emit JackpotPayment(gambler, jackpotWin, commit);
            }
    
            // Send the funds to gambler.
            sendFunds(gambler, rollWin + jackpotWin == 0 ? 1 wei : rollWin + jackpotWin, rollWin, commit);
        }
    
        function refundBet(uint commit) external {
            Bet storage bet = bets[commit];
            if (bet.status == 0) {
                refundBetSingle(commit);
                return;
            }
    
            require(bet.status == 1, "bet should be in a 'placed' status");
    
            // Check that bet has already expired.
            require(block.number > bet.placeBlockNumber + BET_EXPIRATION_BLOCKS, "Blockhash can't be queried by EVM.");
    
            // move into 'refunded' status
            bet.status = 3;
    
            uint refundAmount = 0;
            uint unlockAmount = 0;
            uint jackpotFee;
            uint len = bet.rawBet.length;
            uint modulo = bet.modulo;
            for (uint256 i = 0; i < len; i++) {
                RawBet memory rb = bet.rawBet[i];
                //szabo -> wei
                uint amount = uint(rb.amount) * 10 ** 12;
                uint possibleWinAmount;
                (possibleWinAmount, jackpotFee) = getWinAmount(amount, rb.rollUnder, modulo, jackpotFee);
                unlockAmount += possibleWinAmount;
                refundAmount += amount;
            }
    
            // Unlock the bet amount, regardless of the outcome.
            lockedInBets -= uint128(unlockAmount);
            if (jackpotSize >= jackpotFee) {
                jackpotSize -= uint128(jackpotFee);
            }
    
            // Send the refund.
            sendFunds(bet.gambler, refundAmount, refundAmount, commit);
        }
    
        /////////////////////////////////////////////////////
        /////////////////////////////////////////////////////
        /////////////////////////////////////////////////////
    
        function placeBetSingle(uint betMask, uint modulo, uint commitLastBlock, uint commit, bytes32 r, bytes32 s, uint source) public payable {
            require(!stopped, "contract stopped");
            SingleBet storage bet = singleBets[commit];
    
            // Check that the bet is in 'clean' state.
            require(bet.status == 0 && bets[commit].status == 0, "Bet should be in a 'clean' state.");
    
            // Validate input data ranges.
            uint amount = msg.value;
            require(modulo >= 2 && modulo <= MAX_MODULO, "Modulo should be within range.");
            // 2**(8*9) wei   / 10**18 = 4722 ether
            require(amount >= MIN_BET && amount <= 4721 ether, "Amount should be within range.");
            require(betMask > 0 && betMask < MAX_BET_MASK, "Mask should be within range.");
    
            // Check that commit is valid - it has not expired and its signature is valid.
            require(block.number <= commitLastBlock, "Commit has expired.");
            bytes32 signatureHash = keccak256(abi.encodePacked(commitLastBlock, commit));
            require(secretSigner == ecrecover(signatureHash, 27, r, s), "ECDSA signature is not valid.");
    
            uint rollUnder;
    
            if (modulo <= MASK_MODULO_40) {
                // Small modulo games specify bet outcomes via bit mask.
                // rollUnder is a number of 1 bits in this mask (population count).
                // This magic looking formula is an efficient way to compute population
                // count on EVM for numbers below 2**40.
                rollUnder = ((betMask * POPCNT_MULT) & POPCNT_MASK) % POPCNT_MODULO;
                bet.mask = uint216(betMask);
            } else if (modulo <= MASK_MODULO_40 * 2) {
                rollUnder = getRollUnder(betMask, 2);
                bet.mask = uint216(betMask);
            } else if (modulo == 100) {
                require(betMask > 0 && betMask <= modulo, "modulo=100: betMask larger than modulo");
                rollUnder = betMask;
                bet.mask = uint216(betMask);
            } else if (modulo <= MASK_MODULO_40 * 3) {
                rollUnder = getRollUnder(betMask, 3);
                bet.mask = uint216(betMask);
            } else if (modulo <= MASK_MODULO_40 * 4) {
                rollUnder = getRollUnder(betMask, 4);
                bet.mask = uint216(betMask);
            } else if (modulo <= MASK_MODULO_40 * 5) {
                rollUnder = getRollUnder(betMask, 5);
                bet.mask = uint216(betMask);
            } else {//if (modulo <= MAX_MODULO)
                rollUnder = getRollUnder(betMask, 6);
                bet.mask = uint216(betMask);
            }
    
            // Winning amount and jackpot increase.
            uint possibleWinAmount;
            uint jackpotFee;
    
            //        emit DebugUint("rollUnder", rollUnder);
            (possibleWinAmount, jackpotFee) = getWinAmount(amount, rollUnder, modulo, jackpotFee);
            require(possibleWinAmount > 0, "invalid rollUnder -> zero possibleWinAmount");
    
            // Enforce max profit limit.
            require(possibleWinAmount <= amount + maxProfit, "maxProfit limit violation.");
    
            // Lock funds.
            lockedInBets += uint128(possibleWinAmount);
            jackpotSize += uint128(jackpotFee);
    
            // Check whether contract has enough funds to process this bet.
            require(jackpotSize + lockedInBets <= address(this).balance, "Cannot afford to lose this bet.");
    
            // Record commit in logs.
            emit Commit(commit, source);
    
            // Store bet parameters on blockchain.
            bet.amount = uint72(amount);
            bet.modulo = uint8(modulo);
            bet.rollUnder = uint8(rollUnder);
            bet.placeBlockNumber = uint40(block.number);
            bet.gambler = msg.sender;
            bet.status = 1;
        }
    
        function settleBetSingle(uint reveal, bytes32 blockHash) external onlyCroupier {
            uint commit = uint(keccak256(abi.encodePacked(reveal)));
            SingleBet storage bet = singleBets[commit];
            {
                uint placeBlockNumber = bet.placeBlockNumber;
                require(blockhash(placeBlockNumber) == blockHash, "blockHash invalid");
                require(block.number > placeBlockNumber, "settleBet in the same block as placeBet, or before.");
                require(block.number <= placeBlockNumber + BET_EXPIRATION_BLOCKS, "Blockhash can't be queried by EVM.");
            }
            // Fetch bet parameters into local variables (to save gas).
            uint amount = bet.amount;
            uint modulo = bet.modulo;
            uint rollUnder = bet.rollUnder;
            address payable gambler = bet.gambler;
    
            // Check that bet is in 'active' state.
            require(bet.status == 1, "Bet should be in an 'active' state");
    
            // move into 'settled' status
            bet.status = 2;
    
            // The RNG - combine "reveal" and blockhash of placeBet using Keccak256. Miners
            // are not aware of "reveal" and cannot deduce it from "commit" (as Keccak256
            // preimage is intractable), and house is unable to alter the "reveal" after
            // placeBet have been mined (as Keccak256 collision finding is also intractable).
            bytes32 entropy = keccak256(abi.encodePacked(reveal, blockHash));
    
            // Do a roll by taking a modulo of entropy. Compute winning amount.
            uint dice = uint(entropy) % modulo;
    
            (uint diceWinAmount, uint jackpotFee) = getWinAmount(amount, rollUnder, modulo, 0);
    
            uint diceWin = 0;
            uint jackpotWin = 0;
    
            // Determine dice outcome.
            if (modulo == 216 && 211 <= rollUnder && rollUnder <= 216) {
                uint matchDice = rollUnder - 210;
                uint[] memory dices = tripleDicesTable(dice);
                uint count = 0;
                for (uint ii = 0; ii < 3; ii++) {
                    if (matchDice == dices[ii]) {
                        count++;
                    }
                }
                if (count == 1) {
                    diceWin += amount * (1 + 1);
                } else if (count == 2) {
                    diceWin += amount * (1 + 2);
                } else if (count == 3) {
                    diceWin += amount * (1 + 3);
                }
            } else if (modulo == 100) {
                // For larger modulos, check inclusion into half-open interval.
                if (dice < rollUnder) {
                    diceWin = diceWinAmount;
                }
            } else {
                // For small modulo games, check the outcome against a bit mask.
                if ((2 ** dice) & bet.mask != 0) {
                    diceWin = diceWinAmount;
                }
            }
    
            // Unlock the bet amount, regardless of the outcome.
            lockedInBets -= uint128(diceWinAmount);
    
            // Roll for a jackpot (if eligible).
            if (jackpotFee > 0) {
                // The second modulo, statistically independent from the "main" dice roll.
                // Effectively you are playing two games at once!
                uint jackpotRng = (uint(entropy) / modulo) % JACKPOT_MODULO;
    
                // Bingo!
                if (jackpotRng == 888 || IS_DEV) {
                    jackpotWin = jackpotSize;
                    jackpotSize = 0;
                }
            }
    
            // Log jackpot win.
            if (jackpotWin > 0) {
                emit JackpotPayment(gambler, jackpotWin, commit);
            }
    
            // Send the funds to gambler.
            sendFunds(gambler, diceWin + jackpotWin == 0 ? 1 wei : diceWin + jackpotWin, diceWin, commit);
        }
    
        function refundBetSingle(uint commit) private {
            // Check that bet is in 'active' state.
            SingleBet storage bet = singleBets[commit];
            uint amount = bet.amount;
    
            require(bet.status == 1, "bet should be in a 'placed' status");
    
            // Check that bet has already expired.
            require(block.number > bet.placeBlockNumber + BET_EXPIRATION_BLOCKS, "Blockhash can't be queried by EVM.");
    
            // move into 'refunded' status
            bet.status = 3;
    
            uint diceWinAmount;
            uint jackpotFee;
            (diceWinAmount, jackpotFee) = getWinAmount(amount, bet.rollUnder, bet.modulo, 0);
    
            lockedInBets -= uint128(diceWinAmount);
            if (jackpotSize >= jackpotFee) {
                jackpotSize -= uint128(jackpotFee);
            }
    
            // Send the refund.
            sendFunds(bet.gambler, amount, amount, commit);
        }
    }
    
    /* triple dices table
       1: 1 1 1
       2: 1 1 2
       3: 1 1 3
       4: 1 1 4
       5: 1 1 5
       6: 1 1 6
       7: 1 2 1
       8: 1 2 2
       9: 1 2 3
      10: 1 2 4
      11: 1 2 5
      12: 1 2 6
      13: 1 3 1
      14: 1 3 2
      15: 1 3 3
      16: 1 3 4
      17: 1 3 5
      18: 1 3 6
      19: 1 4 1
      20: 1 4 2
      21: 1 4 3
      22: 1 4 4
      23: 1 4 5
      24: 1 4 6
      25: 1 5 1
      26: 1 5 2
      27: 1 5 3
      28: 1 5 4
      29: 1 5 5
      30: 1 5 6
      31: 1 6 1
      32: 1 6 2
      33: 1 6 3
      34: 1 6 4
      35: 1 6 5
      36: 1 6 6
      37: 2 1 1
      38: 2 1 2
      39: 2 1 3
      40: 2 1 4
      41: 2 1 5
      42: 2 1 6
      43: 2 2 1
      44: 2 2 2
      45: 2 2 3
      46: 2 2 4
      47: 2 2 5
      48: 2 2 6
      49: 2 3 1
      50: 2 3 2
      51: 2 3 3
      52: 2 3 4
      53: 2 3 5
      54: 2 3 6
      55: 2 4 1
      56: 2 4 2
      57: 2 4 3
      58: 2 4 4
      59: 2 4 5
      60: 2 4 6
      61: 2 5 1
      62: 2 5 2
      63: 2 5 3
      64: 2 5 4
      65: 2 5 5
      66: 2 5 6
      67: 2 6 1
      68: 2 6 2
      69: 2 6 3
      70: 2 6 4
      71: 2 6 5
      72: 2 6 6
      73: 3 1 1
      74: 3 1 2
      75: 3 1 3
      76: 3 1 4
      77: 3 1 5
      78: 3 1 6
      79: 3 2 1
      80: 3 2 2
      81: 3 2 3
      82: 3 2 4
      83: 3 2 5
      84: 3 2 6
      85: 3 3 1
      86: 3 3 2
      87: 3 3 3
      88: 3 3 4
      89: 3 3 5
      90: 3 3 6
      91: 3 4 1
      92: 3 4 2
      93: 3 4 3
      94: 3 4 4
      95: 3 4 5
      96: 3 4 6
      97: 3 5 1
      98: 3 5 2
      99: 3 5 3
     100: 3 5 4
     101: 3 5 5
     102: 3 5 6
     103: 3 6 1
     104: 3 6 2
     105: 3 6 3
     106: 3 6 4
     107: 3 6 5
     108: 3 6 6
     109: 4 1 1
     110: 4 1 2
     111: 4 1 3
     112: 4 1 4
     113: 4 1 5
     114: 4 1 6
     115: 4 2 1
     116: 4 2 2
     117: 4 2 3
     118: 4 2 4
     119: 4 2 5
     120: 4 2 6
     121: 4 3 1
     122: 4 3 2
     123: 4 3 3
     124: 4 3 4
     125: 4 3 5
     126: 4 3 6
     127: 4 4 1
     128: 4 4 2
     129: 4 4 3
     130: 4 4 4
     131: 4 4 5
     132: 4 4 6
     133: 4 5 1
     134: 4 5 2
     135: 4 5 3
     136: 4 5 4
     137: 4 5 5
     138: 4 5 6
     139: 4 6 1
     140: 4 6 2
     141: 4 6 3
     142: 4 6 4
     143: 4 6 5
     144: 4 6 6
     145: 5 1 1
     146: 5 1 2
     147: 5 1 3
     148: 5 1 4
     149: 5 1 5
     150: 5 1 6
     151: 5 2 1
     152: 5 2 2
     153: 5 2 3
     154: 5 2 4
     155: 5 2 5
     156: 5 2 6
     157: 5 3 1
     158: 5 3 2
     159: 5 3 3
     160: 5 3 4
     161: 5 3 5
     162: 5 3 6
     163: 5 4 1
     164: 5 4 2
     165: 5 4 3
     166: 5 4 4
     167: 5 4 5
     168: 5 4 6
     169: 5 5 1
     170: 5 5 2
     171: 5 5 3
     172: 5 5 4
     173: 5 5 5
     174: 5 5 6
     175: 5 6 1
     176: 5 6 2
     177: 5 6 3
     178: 5 6 4
     179: 5 6 5
     180: 5 6 6
     181: 6 1 1
     182: 6 1 2
     183: 6 1 3
     184: 6 1 4
     185: 6 1 5
     186: 6 1 6
     187: 6 2 1
     188: 6 2 2
     189: 6 2 3
     190: 6 2 4
     191: 6 2 5
     192: 6 2 6
     193: 6 3 1
     194: 6 3 2
     195: 6 3 3
     196: 6 3 4
     197: 6 3 5
     198: 6 3 6
     199: 6 4 1
     200: 6 4 2
     201: 6 4 3
     202: 6 4 4
     203: 6 4 5
     204: 6 4 6
     205: 6 5 1
     206: 6 5 2
     207: 6 5 3
     208: 6 5 4
     209: 6 5 5
     210: 6 5 6
     211: 6 6 1
     212: 6 6 2
     213: 6 6 3
     214: 6 6 4
     215: 6 6 5
     216: 6 6 6
    */