0

As I was going through the test for the Dice2Win smart contract, It was required to verify few tests, which failed throwing invalid opcode error. The following is the error after running the truffle test is shown below, the contract can be found here "Dice2Win".

pragma solidity >=0.4.21 <0.7.0;

contract Dice {
    uint constant HOUSE_EDGE_PERCENT = 1;
    uint constant HOUSE_EDGE_MINIMUM_AMOUNT = 0.0003 ether;
    uint constant MIN_JACKPOT_BET = 0.1 ether;
    uint constant JACKPOT_MODULO = 1000;
    uint constant JACKPOT_FEE = 0.001 ether;
    uint constant MIN_BET = 0.01 ether;
    uint constant MAX_AMOUNT = 300000 ether;
    uint constant MAX_MODULO = 100;
    uint constant MAX_MASK_MODULO = 40;
    uint constant MAX_BET_MASK = 2 ** MAX_MASK_MODULO;
    uint constant BET_EXPIRATION_BLOCKS = 250;
    address constant DUMMY_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
    address public owner;
    address public nextOwner;
    uint public maxProfit;
    address public secretSigner;
    uint128 public jackpotSize;
    uint128 public lockedInBets;

    // A structure representing a single bet.
    struct Bet {
        // Wager amount in wei.
        uint amount;
        // Modulo of a game.
        uint8 modulo;
        // Number of winning outcomes, used to compute winning payment (* modulo/rollUnder),
        // and used instead of mask for games with modulo > MAX_MASK_MODULO.
        uint8 rollUnder;
        // Block number of placeBet tx.
        uint40 placeBlockNumber;
        // Bit mask representing winning bet outcomes (see MAX_MASK_MODULO comment).
        uint40 mask;
        // Address of a gambler, used to pay out winning bets.
        address payable gambler;
    }

    // Mapping from commits to all currently active & processed bets.
    mapping (uint => Bet) bets;

    // Croupier account.
    address public croupier;

    // Events that are issued to make statistic recovery easier.
    event FailedPayment(address indexed beneficiary, uint amount);
    event Payment(address indexed beneficiary, uint amount);
    event JackpotPayment(address indexed beneficiary, uint amount);

    // This event is emitted in placeBet to record commit in the logs.
    event BetPlaced(
        uint commit,
        address indexed gambler,
        uint amount,
        uint betMask,
        uint modulo
    );
    event OnCommit(uint commit);

    // Constructor. Deliberately does not take any parameters.
    constructor () public {
        owner = msg.sender;
        secretSigner = DUMMY_ADDRESS;
        croupier = DUMMY_ADDRESS;
    }

    // Standard modifier on methods invokable only by contract owner.
    modifier onlyOwner {
        require (msg.sender == owner, "OnlyOwner methods called by non-owner.");
        _;
    }

    // Standard modifier on methods invokable only by contract owner.
    modifier onlyCroupier {
        require (msg.sender == croupier, "OnlyCroupier methods called by non-croupier.");
        _;
    }

    // Standard contract ownership transfer implementation,
    function approveNextOwner(address payable _nextOwner) external onlyOwner {
        require (_nextOwner != owner, "Cannot approve current owner.");
        nextOwner = _nextOwner;
    }

    function acceptNextOwner() external {
        require (msg.sender == nextOwner, "Can only accept preapproved new owner.");
        owner = nextOwner;
    }

    // Fallback function deliberately left empty. It's primary use case
    // is to top up the bank roll.
    function () external payable {
    }

    // 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(uint _maxProfit) public onlyOwner {
        require (_maxProfit < MAX_AMOUNT, "maxProfit should be a sane number.");
        maxProfit = _maxProfit;
    }

    // This function is used to bump up the jackpot fund. Cannot be used to lower it.
    function increaseJackpot(uint increaseAmount) external onlyOwner {
        require (increaseAmount <= address(this).balance, "Increase amount larger than balance.");
        require (jackpotSize + lockedInBets + increaseAmount <= address(this).balance, "Not enough funds.");
        jackpotSize += uint128(increaseAmount);
    }

    // Funds withdrawal to cover costs of dice2.win operation.
    function withdrawFunds(address payable beneficiary, uint withdrawAmount) external onlyOwner {
        require (withdrawAmount <= address(this).balance, "Increase amount larger than balance.");
        require (jackpotSize + lockedInBets + withdrawAmount <= address(this).balance, "Not enough funds.");
        sendFunds(beneficiary, withdrawAmount, withdrawAmount);
    }

    // Contract may be destroyed only when there are no ongoing bets,
    // either settled or refunded. All funds are transferred to contract owner.
    function kill() external onlyOwner {
        require (lockedInBets == 0, "All bets should be processed (settled or refunded) before self-destruct.");
        selfdestruct(msg.sender);
    }
    // event LogSignature (bytes32 hash, uint commit, address signer);
    // // Verify signature of provided data
     function verifySignature(uint commitLastBlock, uint commit, uint8 v, bytes32 r, bytes32 s) private view returns (bool) {
        bytes memory prefix = "\x19Ethereum Signed Message:\n32";
        bytes32 hash = keccak256(abi.encodePacked(uint40(commitLastBlock), commit));
        bytes32 signatureHash = keccak256(abi.encodePacked(prefix, hash));

        return secretSigner == ecrecover(signatureHash, v, r, s);
    }
      function placeBet(uint betMask, uint modulo, uint commitLastBlock, uint commit, uint8 v, bytes32 r, bytes32 s) external payable {
        // Check that the bet is in 'clean' state.
        Bet storage bet = bets[commit];
        require (bet.gambler == address(0), "Bet should be in a 'clean' state.");

        // Validate input data ranges.
        uint amount = msg.value;
        require (modulo > 1 && modulo <= MAX_MODULO, "Modulo should be within range.");
        require (amount >= MIN_BET && amount <= MAX_AMOUNT, "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.");
        require (verifySignature(commitLastBlock, commit, v, r, s), "ECDSA signature is not valid.");
        uint rollUnder;
        uint mask;

        if (modulo <= MAX_MASK_MODULO) {

            rollUnder = ((betMask * POPCNT_MULT) & POPCNT_MASK) % POPCNT_MODULO;
            mask = betMask;
        } else {
            // Larger modulos specify the right edge of half-open interval of
            // winning bet outcomes.
            require (betMask > 0 && betMask <= modulo, "High modulo range, betMask larger than modulo.");
            rollUnder = betMask;
        }

        // Winning amount and jackpot increase.
        uint possibleWinAmount;
        uint jackpotFee;

        (possibleWinAmount, jackpotFee) = getDiceWinAmount(amount, modulo, rollUnder);

        // 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.");

        // Store bet parameters on blockchain.
        bet.amount = amount;
        bet.modulo = uint8(modulo);
        bet.rollUnder = uint8(rollUnder);
        bet.placeBlockNumber = uint40(block.number);
        bet.mask = uint40(mask);
        bet.gambler = msg.sender;

        //Record bet in logs.
       emit OnCommit(commit);
       emit BetPlaced(commit, bet.gambler, bet.amount, bet.mask, bet.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;

        // Check that bet has not expired yet (see comment to BET_EXPIRATION_BLOCKS).
        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 (blockhash(placeBlockNumber) == blockHash, "Blockhash");

        // Settle bet using reveal and blockHash as entropy sources.
        settleBetCommon(bet, reveal, blockHash);
    }

    // Common settlement code for settleBet & settleBetUncleMerkleProof.
    function settleBetCommon(Bet storage bet, uint reveal, bytes32 entropyBlockHash) private {
        // 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 (amount != 0, "Bet should be in an 'active' state");

        // Move bet into 'processed' state already.
        bet.amount = 0;


        bytes32 entropy = keccak256(abi.encodePacked(reveal, entropyBlockHash));

        // Do a roll by taking a modulo of entropy. Compute winning amount.
        uint dice = uint(entropy) % modulo;

        uint diceWinAmount;
        uint _jackpotFee;
        (diceWinAmount, _jackpotFee) = getDiceWinAmount(amount, modulo, rollUnder);

        uint diceWin = 0;
        uint jackpotWin = 0;

        // Determine dice outcome.
        if (modulo <= MAX_MASK_MODULO) {
            // For small modulo games, check the outcome against a bit mask.
            if ((2 ** dice) & bet.mask != 0) {
                diceWin = diceWinAmount;
            }

        } else {
            // For larger modulos, check inclusion into half-open interval.
            if (dice < rollUnder) {
                diceWin = diceWinAmount;
            }

        }

        // Unlock the bet amount, regardless of the outcome.
        lockedInBets -= uint128(diceWinAmount);

        // Roll for a jackpot (if eligible).
        if (amount >= MIN_JACKPOT_BET) {
            // 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 == 0) {
                jackpotWin = jackpotSize;
                jackpotSize = 0;
            }
        }

        // Log jackpot win.
        if (jackpotWin > 0) {
            emit JackpotPayment(gambler, jackpotWin);
        }

        // Send the funds to gambler.
        sendFunds(gambler, diceWin + jackpotWin == 0 ? 1 wei : diceWin + jackpotWin, diceWin);
    }


    function refundBet(uint commit) external {
        // Check that bet is in 'active' state.
        Bet storage bet = bets[commit];
        uint amount = bet.amount;

        require (amount != 0, "Bet should be in an 'active' state");

        // Check that bet has already expired.
        require (block.number > bet.placeBlockNumber + BET_EXPIRATION_BLOCKS, "Blockhash can't be queried by EVM.");

        // Move bet into 'processed' state, release funds.
        bet.amount = 0;

        uint diceWinAmount;
        uint jackpotFee;
        (diceWinAmount, jackpotFee) = getDiceWinAmount(amount, bet.modulo, bet.rollUnder);

        lockedInBets -= uint128(diceWinAmount);
        jackpotSize -= uint128(jackpotFee);

        // Send the refund.
        sendFunds(bet.gambler, amount, amount);
    }

    // Get the expected win amount after house edge is subtracted.
    function getDiceWinAmount(uint amount, uint modulo, uint rollUnder) private pure returns (uint winAmount, uint jackpotFee) {
        require (0 < rollUnder && rollUnder <= modulo, "Win probability out of range.");

        jackpotFee = amount >= MIN_JACKPOT_BET ? JACKPOT_FEE : 0;

        uint houseEdge = amount * HOUSE_EDGE_PERCENT / 100;

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

    // Helper routine to process the payment.
    function sendFunds(address payable beneficiary, uint amount, uint successLogAmount) private {
        if (beneficiary.send(amount)) {
            emit Payment(beneficiary, successLogAmount);
        } else {
            emit FailedPayment(beneficiary, amount);
        }
    }

    // This are some constants making O(1) population count in placeBet possible.
    // See whitepaper for intuition and proofs behind it.
    uint constant POPCNT_MULT = 0x0000000000002000000000100000000008000000000400000000020000000001;
    uint constant POPCNT_MASK = 0x0001041041041041041041041041041041041041041041041041041041041041;
    uint constant POPCNT_MODULO = 0x3F;
}

The error which I got:

1) Contract: Dice
       Verify to make a bet
         should verify Secret signer signature:
     Returned error: VM Exception while processing transaction: invalid opcode

The corresponding test script I used is given below:

const lodash = require('lodash');
const Dice = artifacts.require('./Dice.sol');

contract('Dice', accounts => {
    const owner = accounts[0];
    const secretSigner = accounts[1];
    const croupier = accounts[1];
    const gambler = accounts[2];

    describe('Verify to make a bet', () => {
        let diceInstance;
        const secret = 3;
        let blockHash = null;

        before(async () => {
            diceInstance = await Dice.new();

            await diceInstance.setSecretSigner(secretSigner);
            await diceInstance.setCroupier(croupier);
            await diceInstance.setMaxProfit(web3.utils.toWei('200', 'ether'));
        });

        it('should verify Secret signer signature', async () => {
            const commitLastBlock = await web3.eth.getBlockNumber() + 200;
            console.log("1. commitLastBlock:", commitLastBlock.toString());

            const packedCommit = '0x' + lodash.padStart(secret.toString(16), 64, 0);
            console.log("2. packedCommit:", packedCommit.toString());

            const commit = await web3.utils.sha3(packedCommit, {
                encoding: 'hex',
            });
            console.log("3. commit:", commit.toString());

            const packed = [
                '0x',
                lodash.padStart(commitLastBlock.toString(16), 10, 0),
                commit.substr(2),
            ].join('');
            console.log("4. packed:", packed.toString());

            const hash = await web3.utils.sha3(packed, {
                encoding: 'hex',
            });
            console.log("5. hash:", hash.toString());
            console.log("----secretSigner:", secretSigner.toString());
            const commitSignature = await web3.eth.sign(hash, secretSigner);
            console.log("6. commitSignature:", commitSignature.toString());

            let r = `0x${commitSignature.toString().slice(2, 66)}`
            console.log("7. r value:", r.toString());
            let s = `0x${commitSignature.toString().slice(66, 130)}`
            console.log("8. s value:", s.toString());
            let v = await web3.utils.toDecimal(`0x${commitSignature.toString().slice(130, 132)}`)  + 27;;
            console.log("9. v value:", v.toString());
            // Send some ether to the contract
            await diceInstance.send(web3.utils.toWei('1', 'ether'), { from: owner });

            await diceInstance
                    .placeBet(1, 6, commitLastBlock, commit, v, r, s, {
                    from: gambler,
                    value: web3.utils.toWei('0.1', 'ether'),
                })
                .then(result => {
                    blockHash = result.receipt.blockHash;

                    assert.equal(result.logs.length, 1, 'triggers one event');
                    assert.equal(
                        result.logs[0].event,
                        'BetPlaced',
                        'should be "BetPlaced" event',
                    );
                    assert.equal(
                        result.logs[0].args.gambler,
                        gambler,
                        'logs the account the bet is placed from',
                    );
                    assert.equal(
                        result.logs[0].args.amount.toNumber(),
                        web3.utils.toWei('0.1'),
                        'logs the amount placed on the bet',
                    );
                    assert.equal(
                        result.logs[0].args.betMask.toNumber(),
                        1,
                        'logs the bet mask',
                    );
                    assert.equal(
                        result.logs[0].args.modulo.toNumber(),
                        6,
                        'logs the modulo',
                    );
                });

         });
    }); 
    });


The steps I followed after which I get the error, after placing the files:

  1. truffle compile
  2. truffle migrate -- network ganache
  3. truffle test

I used Ganache GUI for the environment setup. I could see the signer got verified and the transactions occurred in the GUI, but couldn't get the result for the placeBet() instance in the test, as it throws invalid opcode error.

I would appreciate discussions over this. Kindly advice.

8
  • What is POPCNT_MODULO? Commented Apr 20, 2020 at 7:04
  • This are some constants making O(1) population count in placeBet possible.
    – Aldo
    Commented Apr 20, 2020 at 7:24
  • as stated in the source code github.com/dice2-win/contracts/blob/master/Dice2Win.sol#L455
    – Aldo
    Commented Apr 20, 2020 at 7:25
  • No, I meant, what is the value of this constant. In the code that you've provided, the only possible reason for Invalid Opcode is in the % POPCNT_MODULO part, if it this constant value is 0. Commented Apr 20, 2020 at 7:25
  • Please don't link source code here. Make sure that you post ALL the relevant details and ONLY the relevant details in plain (copy/pastable) text. Commented Apr 20, 2020 at 7:27

1 Answer 1

1

I see two places in the code which can throw Invalid Opcode:

  1. uint(entropy) % modulo will throw Invalid Opcode if modulo == 0
  2. uint(entropy) / modulo will throw Invalid Opcode if modulo == 0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.