Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions src/ERC4337Checker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
97 changes: 97 additions & 0 deletions test/ERC4337Checker.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
42 changes: 40 additions & 2 deletions test/mocks/MockAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand All @@ -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;
}
Expand All @@ -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;
Expand Down