-
Notifications
You must be signed in to change notification settings - Fork 2
Description
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
-
Clone the reproduction repositories:
git clone https://github.com/papermoonio/enum-poc git clone https://github.com/papermoonio/safe-smart-account-revm
-
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 localError 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:
- Caller passes
operation=2(out of bounds for enum with only 2 variants) - Solidity compiles enum to WASM
br_tableinstruction with jump table - On standard EVM: May succeed in Solidity 0.7.x (no automatic bounds check) or panic in 0.8+
- On revm:
br_tablewith out-of-bounds index → unreachable trap →InvalidInstruction
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")
})