Skip to content

Latest commit

 

History

History
130 lines (95 loc) · 5.8 KB

README.md

File metadata and controls

130 lines (95 loc) · 5.8 KB

ERC4337-Checker

This is a tool to help validate the ERC4337 limitations on forbidden opcodes and accessing disallowed storage. For detailed specification, please read: EIP4337 spec doc.

This is a library tool intended to be used as a dependency and imported to use in the Forge tests.

The tool is only compatible with the reference implementation of account abstraction: https://github.com/eth-infinitism/account-abstraction/. Currently, this tool is tested with the v0.6.0 version of the account-abstraction repo.

To use this tool, you will need to use a Foundry version after nightly-f79c53c4e41958809ee1f3473466f184bb34c195, which includes the startDebugTraceRecording and stopDebugTraceRecording cheatcodes. Running foundryup should get you the latest nightly versions that include the cheatcodes.

Also, you will need to use forge-std after commit 4f57c59 (see: link). You can install the master branch to get the interface:

forge install foundry-rs/forge-std@master

(Note: at the time of this README, the latest forge-std version is v1.9.3, which does not include the change yet. However, it is likely that all releases afterward should have it included. If there is a newer release, there is no need to install with the master tag.)

After the forge and forge-std setup, you can add this repository to your target repo:

forge install quantstamp/erc4337-checker

Now, you can start writing tests leveraging this ERC4337 checker! The tool supports validation on both userOp and bundle levels.

After providing the test, remember to run it with the -vvv flag. The cheatcode implementation requires a "tracer" to be turned on and, unfortunately, there are no other flags to enable the tracer when initiated (see: PR discussion) at this time. There might be follow-up efforts to enable new flags, though (see: comment).

forge test -vvv

Sample Tests

import {Vm} from "forge-std/Vm.sol";
import {EntryPoint} from "account-abstraction/core/EntryPoint.sol";
import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol";
import {UserOperation} from "account-abstraction/interfaces/UserOperation.sol";

import {ERC4337Checker} from "erc4337-checker/src/ERC4337Checker.sol";


Contract YourTest {
    function setUp() public {
        entryPoint = new EntryPoint();
        mockAccount = new MockAccount(entryPoint);
        vm.deal(address(mockAccount), 1 << 128); // give some funds to the mockAccount

        checker = new ERC4337Checker(); // initiate the checker contract
    }

    function testSingleUserOp() public {
        UserOperation memory userOp = getUserOperationAndSign(...) // <-- put your own logic here

        ...skip some codes...

        // a single line that will do all the magic!
        // this function calls `simulateValidation()` and capture the
        // debugging steps and do the validations!
        bool result = checker.simulateAndVerifyUserOp(vm, userOp, entryPoint);

        // (Optional) To debug, this will output all failure logs telling what rules are violated.
        // To see the logs, please run the test with `forge test -vvv`
        // you would not need this usually, only needed when the test failed unexpectedly.
        checker.printFailureLogs();

        assertTrue(result);
    }


    function testBundle() public {
        UserOperation[] memory userOps = getUserOperationsAndSign(...) // <-- put your own logic here

        ...skip some codes...

        // this starts the recording of the debug trace that will later be analyzed
        vm.startDebugTraceRecording();

        // just put all your userOps of the bundle in, and this will run the simulation for all the user ops
        // and check bundle specific rules
        assertTrue(
            checker.simulateAndVerifyBundle(vm, userOps, entryPoint)
        );
    }
}

If the existing tests have self-defined interfaces or structs, one can use import {A as B} from '....' to avoid name collision.

// use AA prefix to avoid collision with the self-defined interfaces/structs later
import {IEntryPoint as AAIEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol";
import {UserOperation as AAUserOperation} from "account-abstraction/interfaces/UserOperation.sol";

...skip...
// collision interfaces/struct
import {IEntryPoint} from "../../src/interfaces/erc4337/IEntryPoint.sol";
import {UserOperation} from "../../src/interfaces/erc4337/UserOperation.sol";

import {ERC4337Checker} from "erc4337-checker/src/ERC4337Checker.sol";
import {Vm} from "forge-std/Vm.sol";

contract YourTest {
    function test_simulateValidation_basicUserOp() public {
        ...skip code...
        // Self defined UserOperation struct
        UserOperation memory userOp = UserOperation(....);

        ...skip code...

        // Adapt to the AAUserOperation using the struct from the `eth-infinitism/account-abstraction`
        AAUserOperation memory aaUserOp = AAUserOperation({
            sender: userOp.sender,
            nonce: userOp.nonce,
            initCode: userOp.initCode,
            callData: userOp.callData,
            callGasLimit: userOp.callGasLimit,
            verificationGasLimit: userOp.verificationGasLimit,
            preVerificationGas: userOp.preVerificationGas,
            maxFeePerGas: userOp.maxFeePerGas,
            maxPriorityFeePerGas: userOp.maxPriorityFeePerGas,
            paymasterAndData: userOp.paymasterAndData,
            signature: userOp.signature
        });


        assertTrue(checker.simulateAndVerifyUserOp(vm, aaUserOp, entryPoint));
    }
}