7

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.

2 Answers 2

6

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.

10
  • I agree with your assessment! I used a fresh remix environment and put nothing more than the code pasted above in there. It should be very easy to replicate.
    – Newti
    Mar 5, 2020 at 11:07
  • @Newti: Check to see if it works fine when you downgrade to solc 0.4.25 for example. Mar 5, 2020 at 11:17
  • 1
    @Newti: And BTW, if solc 0.6.x indeed handles illegal enum values, then it means that you can safely remove the require statement, as this verification is implicitly conducted for you. I find it odd that those guys at solc would use INVALID instead of REVERT, because it essentially means that the caller would pay more gas (plus, INVALID opcode is not designated for incorrect user input, while REVERT opcode is designated exactly for that). Mar 5, 2020 at 11:22
  • I tested with a few older solc versions (including 0.4.25) and got the same INVALID opcode every time. I agree that it should REVERT instead. Letting the compiler handle it (and removing the require) sounds like the cleaner solution than converting ints to enums. So the question now is why solc does not use REVERT...
    – Newti
    Mar 5, 2020 at 11:29
  • @Newti: I'm pretty sure that solc 0.4.x does not apply this assertion on enum values, and if I'm right, then it implies that the INVALID opcode is due to something else in your code or in your setup. I don't see anything that could trigger this error in the code, so I suspect that this issue is related to your setup. Mar 5, 2020 at 11:40
2

Enum can be safely used as an input to external functions, but (as discussed earlier) throws panic instead of error if the uint8 value is out of scope.

You can take a look into docs https://docs.soliditylang.org/en/v0.8.8/control-structures.html?highlight=panic%20error#panic-via-assert-and-error-via-require and find out that 0x21 error code is dedicated to enum conversions.

From one point of view, it's an input issue, from another it is us trying to convert a value to enum that is out of scope. That is probably the reason why it's not considered an input issue.

Your Answer

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

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