[Ethereum] How to handle enums as arguments for public functions

invalid-opcodesolidity

Assume this simple contract:

pragma solidity 0.6.3;

contract InvalidOpcode {

    enum Dir {UP, LEFT, DOWN, RIGHT}
    Dir public currentDir;

    function changeDir(Dir newDir) public {
        require(uint8(newDir) <= uint8(Dir.RIGHT), "Out of range");
        currentDir = newDir;
    } 

}

The exposed interface of the function accepts an uint8 as argument.
When run on Remix with a value of 4 (or greater) as the input, the tx will throw with this error:

transact to EnumTest.changeDir errored: VM error: invalid opcode.
invalid opcode  
    The execution might have thrown.
    Debug the transaction to get more information. 

Debugging reveals that the require statement is not reached (and is thus useless). The invalid opcode occurs while accesing the argument. The debugger shows the local variable newDir: INVALID_ENUM<4> enum.
I think the transaction then throws and stops using gas, but I am not sure!

When I run the same thing in a truffle environment, the transaction will exit with status 0.
truffleAssert.fails(instance.changeDir(4),"status 0") will catch it, but all gas will be consumed in the transaction! From what I know, it is not possible to debug further (on opcode level) in truffle.

My questions are:

  1. Does this transaction indeed consume all gas?
  2. If so, is there a way to prevent this?
  3. Is there a way to revert the transaction with a meaningful message?
  4. What is the best practice to expose an Enum argument to the public interface?

Edit:

One way to potentially solve the problem is to change the argument to an uint8:

contract FixedInvalidOpcode {

    enum Dir {UP, LEFT, DOWN, RIGHT}
    Dir public currentDir;

    function changeDir(uint8 newDir) public {
        require(newDir <= uint8(Dir.RIGHT), "Out of range");
        currentDir = Dir(newDir);
    } 

}

This behaves as expected. But is the type conversion Dir(newDir) safe and is this the recommended way to expose the function?

I would still like to get into what is happening if an enum is directly exposed to the interface and if that is something that can be improved in the compiler. In my opinion, the first contract above should revert with a failing assert.

Best Answer

enum Dir {UP, LEFT, DOWN, RIGHT} corresponds to uint8 values 0, 1, 2 and 3.

This explains why passing a value greater than 3 should cause some form of error.

That said, require(false condition) returns REVERT opcode, not INVALID opcode.

The latter is typically the result of either one of the following:

  • assert(x) where x evaluates to false
  • arr[i], where i >= arr.length

Perhaps solc 0.6.x handles illegal enum values in the same manner.

I would sincerely doubt that, because REVERT opcode (require/revert) is logically designated for incorrect user input, while INVALID opcode (assert) is not.

So I recommend that you check if the error is not due to something outside of your contract (for example, something in your Remix setup).

UPDATE:

OK, it seems to be the case even in older compiler versions (tested 0.4.21).

Here is the relevant assembly code:

    tag_10:
      ...
      0x3     // maximum permitted value
      dup2    // load to register
      gt      // check greater than
      iszero  // if not, then ...
      tag_12
      jumpi   // ... jump to the tag (skip the next instruction)
      invalid // trigger INVALID opcode
    tag_12:
      ...

The good news on your side are that since the compiler handles illegal enum values, you can safely remove the require statement, because this verification is implicitly conducted for you at the very beginning of the function.

Related Topic