From 7837d555425f11502fe35aec03dc4ebccbb31ae9 Mon Sep 17 00:00:00 2001 From: Mberic Date: Tue, 30 Dec 2025 15:38:59 +0300 Subject: [PATCH] feat: op-032 from ERC 7562 spec --- .gitignore | 2 ++ src/ERC4337Checker.sol | 48 ++++++++++++++++++++++++++++++-------- test/ERC4337Checker.t.sol | 19 +++++++++++++++ test/mocks/MockAccount.sol | 7 +++++- 4 files changed, 65 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 85198aa..14e0bf6 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ docs/ # Dotenv file .env + +.vscode \ No newline at end of file diff --git a/src/ERC4337Checker.sol b/src/ERC4337Checker.sol index dd35dbe..6d80b7a 100644 --- a/src/ERC4337Checker.sol +++ b/src/ERC4337Checker.sol @@ -200,7 +200,7 @@ contract ERC4337Checker { } bool result = true; - if (!validateForbiddenOpcodes(debugSteps)) { + if (!validateForbiddenOpcodes(debugSteps, userOp)) { result = false; } if (!validateCall(debugSteps, address(entryPoint), true)) { @@ -298,11 +298,38 @@ contract ERC4337Checker { * May not invokes any forbidden opcodes * Must not use GAS opcode (unless followed immediately by one of { CALL, DELEGATECALL, CALLCODE, STATICCALL }.) */ - function validateForbiddenOpcodes(Vm.DebugStep[] memory debugSteps) private returns (bool) { + function validateForbiddenOpcodes(Vm.DebugStep[] memory debugSteps, UserOperation memory userOp) private returns (bool) { bool result = true; for (uint256 i = 0; i < debugSteps.length; i++) { uint8 opcode = debugSteps[i].opcode; if (isForbiddenOpcode(opcode)) { + + // exception for CREATE opcode + if (opcode == 0xF0){ + // CREATE is only allowed if factory exists AND sender directly executes it + if(getFactoryAddr(userOp) == address(0)){ + failureLogs.push(FailureLog({ + errorMsg: string(abi.encodePacked( + "CREATE opcode forbidden: no factory address present" + )), + contractAddr: debugSteps[i].contractAddr + })); + result = false; + } else if (debugSteps[i].contractAddr != userOp.sender) { + // Factory exists, but CREATE must be directly from sender, not utility contract + failureLogs.push(FailureLog({ + errorMsg: string(abi.encodePacked( + "CREATE opcode forbidden: only sender can execute it. ", + "Expected: [", Strings.toHexString(userOp.sender), "], ", + "Got: [", Strings.toHexString(debugSteps[i].contractAddr), "]" + )), + contractAddr: debugSteps[i].contractAddr + })); + result = false; + } + continue; + } + // exception case for GAS opcode if (opcode == 0x5A && i < debugSteps.length - 1) { if (!isValidNextOpcodeOfGas(debugSteps[i + 1].opcode)) { @@ -314,15 +341,16 @@ contract ERC4337Checker { })); result = false; } - } else { - failureLogs.push(FailureLog({ - errorMsg: string(abi.encodePacked( - "forbidden op-code usage. opcode: [", Strings.toHexString(opcode), "]" - )), - contractAddr: debugSteps[i].contractAddr - })); - result = false; + continue; } + + failureLogs.push(FailureLog({ + errorMsg: string(abi.encodePacked( + "forbidden op-code usage. opcode: [", Strings.toHexString(opcode), "]" + )), + contractAddr: debugSteps[i].contractAddr + })); + result = false; } } return result; diff --git a/test/ERC4337Checker.t.sol b/test/ERC4337Checker.t.sol index c682029..5e99e86 100644 --- a/test/ERC4337Checker.t.sol +++ b/test/ERC4337Checker.t.sol @@ -187,6 +187,25 @@ contract ERC4337CheckerTest is Test { checker.printFailureLogs(); } + function test_forbiddenOpCodeCreate() public { + address mockAccountAddr = address(mockAccount); + + bytes memory encodedCallData = abi.encodeWithSelector( + MockAccount.execute.selector, + MockAccount.AttackType.FORBIDDEN_OPCODE_CREATE + ); + + UserOperation memory userOp = _getUnsignedOp( + mockAccountAddr, + encodedCallData + ); + + 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 d33f6b1..d45e816 100644 --- a/test/mocks/MockAccount.sol +++ b/test/mocks/MockAccount.sol @@ -40,7 +40,8 @@ contract MockAccount is BaseAccount { FORBIDDEN_OPCODE_GASLIMIT, FORBIDDEN_OPCODE_COINBASE, FORBIDDEN_OPCODE_ORIGIN, - FORBIDDEN_OPCODE_INVALID + FORBIDDEN_OPCODE_INVALID, + FORBIDDEN_OPCODE_CREATE } IEntryPoint private _entryPoint; @@ -113,6 +114,10 @@ contract MockAccount is BaseAccount { // The invalid opcode was executed and caught console2.log("Invalid opcode caught in try-catch"); } + } else if (attackType == AttackType.FORBIDDEN_OPCODE_CREATE){ + address demoAddress = address(new InvalidActions()); + console2.log("New contract", demoAddress); + } return 0;