Skip to content

[safe-smart-account-revm] Enum Out-of-Bounds: InvalidInstruction Error in revm #232

@sekisamu

Description

@sekisamu

Description

When Solidity enum types receive out-of-bounds values (e.g., operation=2 for enum Operation {Call=0, DelegateCall=1}), Revive runtime returns an InvalidInstruction error with massive gas consumption instead of properly handling the enum bounds check.

Environment

  • Substrate Revive Node: v0.1.0-cf286f36644
  • Solidity Version: 0.7.6
  • Test Framework: Hardhat
  • Chain ID: 420420420 (Revive local network)

Reproduction Steps

Prerequisites

  1. Clone the reproduction repositories:

    git clone https://github.com/papermoonio/enum-poc
    git clone https://github.com/papermoonio/safe-smart-account-revm
  2. Start Substrate Revive node (under polkadot-sdk project root):

    ./target/release/revive-dev-node --dev
    ./target/release/eth-rpc --dev

Minimal POC (Recommended)

The simplest way to reproduce this issue:

cd enum-poc
npm install
npx hardhat test --network local

Error log:

  Enum Boundary POC - Minimal Test
    Valid enum values
      ✔ should handle operation=0 (Call)
      ✔ should handle operation=1 (DelegateCall)
    THE KEY TEST: Invalid enum value (2)
      1) should expose enum boundary behavior difference
    Comparison: enum vs uint8
   ✅ testRawValue(2) = 'UNKNOWN'
   → uint8 parameter has no enum bounds checking

      ✔ should show testRawValue(2) succeeds


  3 passing (325ms)
  1 failing

  1) Enum Boundary POC - Minimal Test
       THE KEY TEST: Invalid enum value (2)
         should expose enum boundary behavior difference:
     ProviderError: failed to run contract: Module(ModuleError { index: 5, error: [31, 0, 0, 0], message: Some("InvalidInstruction") })
      at HttpProvider.request (/Users/suvi/Documents/paritytech/papermoon/code/enum-poc/node_modules/hardhat/src/internal/core/providers/http.ts:116:21)
      at processTicksAndRejections (node:internal/process/task_queues:105:5)
      at staticCallResult (/Users/suvi/Documents/paritytech/papermoon/code/enum-poc/node_modules/ethers/src.ts/contract/contract.ts:337:22)
      at AsyncFunction.staticCall (/Users/suvi/Documents/paritytech/papermoon/code/enum-poc/node_modules/ethers/src.ts/contract/contract.ts:303:24)
      at Context.<anonymous> (/Users/suvi/Documents/paritytech/papermoon/code/enum-poc/test/EnumTest.js:30:7)

When running npx hardhat test, the result is:

  Enum Boundary POC - Minimal Test
    Valid enum values
      ✓ should handle operation=0 (Call)
      ✓ should handle operation=1 (DelegateCall)
    THE KEY TEST: Invalid enum value (2)
      ✓ should expose enum boundary behavior difference
         ✅ Call reverted as expected
         → Enum bounds checking detected the error
    Comparison: enum vs uint8
      ✓ should show testRawValue(2) succeeds
         ✅ testRawValue(2) = 'UNKNOWN'
         → uint8 parameter has no enum bounds checking

  4 passing (123ms)

For local revm testnet:
Expected Behavior: Test should revert with standard Solidity panic code (caught by hardhat)
Actual Behavior: Transaction fails with InvalidInstruction error

Full Safe Contract POC

To see the issue in Safe contract:

cd safe-smart-account-revm
npm install
npx hardhat test --network local --grep "should revert on unknown operation"

Error log:

  Execution
    1) should revert on unknown operation


  0 passing (1s)
  1 failing

  1) Execution
       should revert on unknown operation:
     ProviderError: failed to run contract: Module(ModuleError { index: 5, error: [31, 0, 0, 0], message: Some("InvalidInstruction") })
      at HttpProvider.request (/Users/suvi/Documents/paritytech/papermoon/code/safe-smart-account-revm/node_modules/hardhat/src/internal/core/providers/http.ts:116:21)
      at processTicksAndRejections (node:internal/process/task_queues:105:5)
      at async estimateGas (/Users/suvi/Documents/paritytech/papermoon/code/safe-smart-account-revm/node_modules/ethers/src.ts/contract/contract.ts:319:30)
      at async Proxy.execTransaction (/Users/suvi/Documents/paritytech/papermoon/code/safe-smart-account-revm/node_modules/ethers/src.ts/contract/contract.ts:379:24)
      at async executeTx (/Users/suvi/Documents/paritytech/papermoon/code/safe-smart-account-revm/src/utils/execution.ts:148:12)
      at async Context.<anonymous> (/Users/suvi/Documents/paritytech/papermoon/code/safe-smart-account-revm/test/core/Safe.Execution.spec.ts:189:13)

When running npx hardhat test --grep "should revert on unknown operation", the result is:

  Execution
    ✓ should revert on unknown operation (45ms)

  1 passing (567ms)

Technical Analysis

Root Cause

The bug occurs when Solidity's enum type receives an out-of-bounds value:

File: enum-poc/contracts/EnumTest.sol

contract EnumTest {
    enum Operation {
        Call,         // 0
        DelegateCall  // 1
        // Value 2 is OUT OF BOUNDS
    }

    function testEnum(Operation op) public returns (string memory) {
        if (op == Operation.Call) {
            return "CALL";
        } else if (op == Operation.DelegateCall) {
            return "DELEGATECALL";
        } else {
            // This branch should never execute if bounds checking works
            return "UNKNOWN";
        }
    }
}

File: safe-smart-account-revm/contracts/libraries/Enum.sol

library Enum {
    enum Operation {
        Call,         // 0
        DelegateCall  // 1
        // Value 2 is OUT OF BOUNDS
    }
}

File: safe-smart-account-revm/contracts/base/Executor.sol

function execute(
    address to,
    uint256 value,
    bytes memory data,
    Enum.Operation operation,  // Expects 0 or 1
    uint256 txGas
) internal returns (bool success) {
    if (operation == Enum.Operation.DelegateCall) {
        // DelegateCall logic
    } else {
        // Call logic
    }
}

File: safe-smart-account-revm/test/core/Safe.Execution.spec.ts:184-190

it("should revert on unknown operation", async () => {
    const { safe, signers } = await setupTests();
    const [user1] = signers;
    const safeAddress = await safe.getAddress();

    // operation: 2 exceeds enum bounds (0, 1)
    const tx = buildSafeTransaction({
        to: safeAddress,
        nonce: await safe.nonce(),
        operation: 2  // ❌ Out of bounds
    });

    await expect(
        executeTx(safe, tx, [await safeApproveHash(user1, safe, tx, true)])
    ).to.be.reverted;
});

What happens:

  1. Caller passes operation=2 (out of bounds for enum with only 2 variants)
  2. Solidity compiles enum to WASM br_table instruction with jump table
  3. On standard EVM: May succeed in Solidity 0.7.x (no automatic bounds check) or panic in 0.8+
  4. On revm: br_table with out-of-bounds index → unreachable trapInvalidInstruction

Node-Side Error Logs

The Substrate Revive node logs show:

2025-12-17 20:13:22.516 TRACE runtime::revive: frame finished with:
  Err(ExecError {
    error: Module(ModuleError {
      index: 5,
      error: [31, 0, 0, 0],
      message: Some("InvalidInstruction")
    }),
    origin: Callee
  })

2025-12-17 20:13:22.516 TRACE runtime::revive: Bare call ends:
  result=Err(ExecError {
    error: Module(ModuleError {
      index: 5,
      error: [31, 0, 0, 0],
      message: Some("InvalidInstruction")
    }),
    origin: Callee
  }),
  weight_consumed=Weight {
    ref_time: 18446744072305004696,
    proof_size: 18446744073709534376
  }

2025-12-17 20:13:22.516 DEBUG runtime::revive: Failed to execute call:
  Module(ModuleError {
    index: 5,
    error: [31, 0, 0, 0],
    message: Some("InvalidInstruction")
  })

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions