diff --git a/foundry.toml b/foundry.toml index 025b841..b305392 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,8 +2,22 @@ src = "src" out = "out" libs = ["lib"] +solc = "0.8.30" +optimizer = true +optimizer_runs = 200 # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options gas_limit = "18446744073709551615" memory_limit = 2147483648 + +[profile.ci] +src = "src" +out = "out" +libs = ["lib"] +solc = "0.8.30" +optimizer = true +optimizer_runs = 200 + +gas_limit = "18446744073709551615" +memory_limit = 2147483648 diff --git a/src/ERC4337Checker.sol b/src/ERC4337Checker.sol index 33f318e..dd35dbe 100644 --- a/src/ERC4337Checker.sol +++ b/src/ERC4337Checker.sol @@ -456,6 +456,8 @@ contract ERC4337Checker { || opcode == 0x44 // DIFFICULTY || opcode == 0x42 // TIMESTAMP || opcode == 0x48 // BASEFEE + || opcode == 0x49 // BLOBHASH + || opcode == 0x4A // BLOBBASEFEE || opcode == 0x40 // BLOCKHASH || opcode == 0x43 // NUMBER || opcode == 0x47 // SELFBALANCE diff --git a/test/ERC4337Checker.t.sol b/test/ERC4337Checker.t.sol index 1e4812b..c682029 100644 --- a/test/ERC4337Checker.t.sol +++ b/test/ERC4337Checker.t.sol @@ -90,6 +90,103 @@ contract ERC4337CheckerTest is Test { checker.printFailureLogs(); } + function test_forbiddenOpCodeGasPrice() public { + address mockAccountAddr = address(mockAccount); + + bytes memory encodedCallData = abi.encodeWithSelector( + MockAccount.execute.selector, + MockAccount.AttackType.FORBIDDEN_OPCODE_GASPRICE + ); + + UserOperation memory userOp = _getUnsignedOp( + mockAccountAddr, + encodedCallData + ); + + assertFalse( + checker.simulateAndVerifyUserOp(vm, userOp, entryPoint) + ); + checker.printFailureLogs(); + } + + function test_forbiddenOpCodeGasLimit() public { + address mockAccountAddr = address(mockAccount); + + bytes memory encodedCallData = abi.encodeWithSelector( + MockAccount.execute.selector, + MockAccount.AttackType.FORBIDDEN_OPCODE_GASLIMIT + ); + + UserOperation memory userOp = _getUnsignedOp( + mockAccountAddr, + encodedCallData + ); + + assertFalse( + checker.simulateAndVerifyUserOp(vm, userOp, entryPoint) + ); + checker.printFailureLogs(); + } + + function test_forbiddenOpCodeCoinbase() public { + address mockAccountAddr = address(mockAccount); + + bytes memory encodedCallData = abi.encodeWithSelector( + MockAccount.execute.selector, + MockAccount.AttackType.FORBIDDEN_OPCODE_COINBASE + ); + + UserOperation memory userOp = _getUnsignedOp( + mockAccountAddr, + encodedCallData + ); + + assertFalse( + checker.simulateAndVerifyUserOp(vm, userOp, entryPoint) + ); + checker.printFailureLogs(); + } + + function test_forbiddenOpCodeOrigin() public { + address mockAccountAddr = address(mockAccount); + + bytes memory encodedCallData = abi.encodeWithSelector( + MockAccount.execute.selector, + MockAccount.AttackType.FORBIDDEN_OPCODE_ORIGIN + ); + + UserOperation memory userOp = _getUnsignedOp( + mockAccountAddr, + encodedCallData + ); + + assertFalse( + checker.simulateAndVerifyUserOp(vm, userOp, entryPoint) + ); + checker.printFailureLogs(); + } + + function test_forbiddenOpCodeInvalid() public { + address mockAccountAddr = address(mockAccount); + + bytes memory encodedCallData = abi.encodeWithSelector( + MockAccount.execute.selector, + MockAccount.AttackType.FORBIDDEN_OPCODE_INVALID + ); + + UserOperation memory userOp = _getUnsignedOp( + mockAccountAddr, + encodedCallData + ); + + // The INVALID opcode (0xFE) is executed in a try-catch block + // This allows the debug trace to capture it, and the checker should detect it + assertFalse( + checker.simulateAndVerifyUserOp(vm, userOp, entryPoint) + ); + checker.printFailureLogs(); + } + function test_outOfGas() public { address mockAccountAddr = address(mockAccount); diff --git a/test/mocks/MockAccount.sol b/test/mocks/MockAccount.sol index 9b07101..d33f6b1 100644 --- a/test/mocks/MockAccount.sol +++ b/test/mocks/MockAccount.sol @@ -20,6 +20,13 @@ contract InvalidActions { function touchInvalidSlot() public view { console2.log("consumeGas: ", invalidSlotAccess); } + + // Function that triggers the INVALID opcode (0xFE) + function triggerInvalidOpcode() public pure { + assembly { + invalid() + } + } } contract MockAccount is BaseAccount { @@ -28,7 +35,12 @@ contract MockAccount is BaseAccount { FORBIDDEN_OPCODE_BLOCKTIME, OUT_OF_GAS, ACCESS_EXTCODE_WITH_ADDRESS_NO_CODE, - TOUCH_UNASSOCIATED_STORAGE_SLOT + TOUCH_UNASSOCIATED_STORAGE_SLOT, + FORBIDDEN_OPCODE_GASPRICE, + FORBIDDEN_OPCODE_GASLIMIT, + FORBIDDEN_OPCODE_COINBASE, + FORBIDDEN_OPCODE_ORIGIN, + FORBIDDEN_OPCODE_INVALID } IEntryPoint private _entryPoint; @@ -55,7 +67,7 @@ contract MockAccount is BaseAccount { { AttackType attackType = _decodeAttackType(userOp.callData); if (attackType == AttackType.FORBIDDEN_OPCODE_BLOCKTIME) { - // forbidden opcode + // forbidden opcode: TIMESTAMP if (block.timestamp < 1) { return 0; } @@ -75,6 +87,32 @@ contract MockAccount is BaseAccount { require(result == bytes32(0), "The EXTCODEHASH of non-existent account should be 0"); } else if (attackType == AttackType.TOUCH_UNASSOCIATED_STORAGE_SLOT) { invalidActions.touchInvalidSlot(); + } else if (attackType == AttackType.FORBIDDEN_OPCODE_GASPRICE) { + // forbidden opcode: GASPRICE + uint256 gasPrice = tx.gasprice; + console2.log("gasprice:", gasPrice); + } else if (attackType == AttackType.FORBIDDEN_OPCODE_GASLIMIT) { + // forbidden opcode: GASLIMIT + uint256 gasLimit = block.gaslimit; + console2.log("gaslimit:", gasLimit); + } else if (attackType == AttackType.FORBIDDEN_OPCODE_COINBASE) { + // forbidden opcode: COINBASE + address coinbase = block.coinbase; + console2.log("coinbase:", coinbase); + } else if (attackType == AttackType.FORBIDDEN_OPCODE_ORIGIN) { + // forbidden opcode: ORIGIN + address origin = tx.origin; + console2.log("origin:", origin); + } else if (attackType == AttackType.FORBIDDEN_OPCODE_INVALID) { + // forbidden opcode: INVALID (0xFE) + // We use try-catch to prevent immediate revert, allowing the debug trace to capture the opcode + try invalidActions.triggerInvalidOpcode() { + // This should never execute as invalid() always reverts + console2.log("Invalid opcode did not revert (unexpected)"); + } catch { + // The invalid opcode was executed and caught + console2.log("Invalid opcode caught in try-catch"); + } } return 0;