Security Audit Checklist for Account Abstraction Wallets

A baseline checklist for auditing account abstraction wallets implemented based on the EIP4337 standard

Introduction

This guide provides auditors with a fundamental checklist for reviewing account abstraction wallets based on the EIP4337 standard, along with targeted auditing guidelines. It assumes auditors are familiar with the EIP4337 Account Abstraction Standard and the EIP7562 Account Abstraction Validation Scope Rules Standard. We'll briefly cover the EIP4337 architecture and wallet transaction execution flow.

Architecture

Transaction Execution

In EIP4337, an EOA signs UserOperation data and submits it to a separate Alt Mempool via RPC. This mempool, distinct from Ethereum's, aggregates user-submitted UserOp data. The Bundler extracts and simulates UserOps locally before execution, discarding failed simulations. All UserOp executions occur through the Bundler calling the EntryPoint contract. After verification, EntryPoint calls the user's AA wallet to execute the user's calldata. Users pay the Bundler for on-chain execution fees or specify a Paymaster to cover costs.

Execution Details

Auditors should understand the process of the Bundler calling the user's wallet via EntryPoint:

Checklist

The following checklist items ensure each 4337 wallet passes crucial security checks:

  1. Verify Compatibility with All EVM-Compatible Chains

AA wallets may deploy on various chains. Post-Shanghai Ethereum mainnet introduced PUSH0 bytecode, affecting Solidity versions 0.8.20+. Auditors should check the Solidity version or compiled files for PUSH0 bytecode. For multi-chain deployment, use a compiler version below 0.8.20 or specify the paris compilation version.

solidityCopysolc = "0.8.19"
evm_version = "paris"
  1. Ensure Interface Implementation and Return Values Comply with EIP4337

Wallets must implement core interfaces with specific return value structures. Paymasters must also implement required interfaces. Signature validation should return appropriate values or revert as specified.

  1. Verify Trusted Wallet Callers

EIP4337 interfaces should only allow trusted EntryPoint calls to prevent unauthorized wallet use.

Example Code:

solidityCopyfunction entryPoint() public view virtual override returns (IEntryPoint) {
   return _entryPoint;
}

function execute(address dest, uint256 value, bytes calldata func) external {
    _requireFromEntryPointOrOwner();
    _call(dest, value, func);
}

function executeBatch(address[] calldata dest, uint256[] calldata value, bytes[] calldata func) external {
    _requireFromEntryPointOrOwner();
    ...
}
  1. Check Fee Payment Implementation

Wallets should implement logic to transfer missingAccountFunds to the EntryPoint contract when necessary.

Example Code:

solidityCopyfunction validateUserOp(
    PackedUserOperation calldata userOp,
    bytes32 userOpHash,
    uint256 missingAccountFunds
) external virtual override returns (uint256 validationData) {
    ...
    _payPrefund(missingAccountFunds);
}
  1. Verify Wallet Creation Method

Factories must use CREATE2 for deterministic wallet creation addresses.

Example Code:

solidityCopyfunction createAccount(address owner,uint256 salt) public returns (SimpleAccount ret) {
    address addr = getAddress(owner, salt);
    uint256 codeSize = addr.code.length;
    if (codeSize > 0) {
        return SimpleAccount(payable(addr));
    }
    ret = SimpleAccount(payable(new ERC1967Proxy{salt : bytes32(salt)}(
            address(accountImplementation),
            abi.encodeCall(SimpleAccount.initialize, (owner))
        )));
}
  1. Check Return Value for Repeated Wallet Creation

Ensure consistent address returns for already-created wallets.

Example Code:

solidityCopyfunction createAccount(address owner,uint256 salt) public returns (SimpleAccount ret) {
    address addr = getAddress(owner, salt);
    uint256 codeSize = addr.code.length;
    if (codeSize > 0) {
        return SimpleAccount(payable(addr));
    }
    ...
}
  1. Prevent Wallet Takeover During Creation

Verify that wallet creation cannot be front-run and that ownership is correctly set.

Example of Incorrect Code: (entryPoint not involved in address calculation, can be takeover and modified to a malicious entryPoint)

function deployCounterFactualWallet(address _owner, address _entryPoint, address _handler, uint _index) public returns(address proxy){
    bytes32 salt = keccak256(abi.encodePacked(_owner, address(uint160(_index))));
    bytes memory deploymentData = abi.encodePacked(type(Proxy).creationCode, uint(uint160(_defaultImpl)));
    // solhint-disable-next-line no-inline-assembly
    assembly {
        proxy := create2(0x0, add(0x20, deploymentData), mload(deploymentData), salt)
    }
    require(address(proxy) != address(0), "Create2 call failed");
    // EOA + Version tracking
    emit SmartAccountCreated(proxy,_defaultImpl,_owner, VERSION, _index);
    BaseSmartAccount(proxy).init(_owner, _entryPoint, _handler);
    isAccountExist[proxy] = true;
}
  1. Validate Signature Verification

Ensure rigorous signature validation in validateUserOp/validatePaymasterUserOp.

Example Code:

solidityCopyfunction validateUserOp(
    PackedUserOperation calldata userOp,
    bytes32 userOpHash,
    uint256 missingAccountFunds
) external virtual override returns (uint256 validationData) {
    _requireFromEntryPoint();
    validationData = _validateSignature(userOp, userOpHash);
    _validateNonce(userOp.nonce);
    _payPrefund(missingAccountFunds);
}
  1. Verify Correct ERC1271 Implementation

Check ERC1271 standard compliance and signature verification logic security.

Example Code:

solidityCopyfunction isValidSignature(bytes32 _dataHash, bytes calldata _signature) public view override returns (bytes4) {
    // Caller should be a Safe
    ISafe safe = ISafe(payable(msg.sender));
    bytes memory messageData = encodeMessageDataForSafe(safe, abi.encode(_dataHash));
    bytes32 messageHash = keccak256(messageData);
    if (_signature.length == 0) {
        require(safe.signedMessages(messageHash) != 0, "Hash not approved");
    } else {
        safe.checkSignatures(messageHash, _signature);
    }
    return EIP1271_MAGIC_VALUE;
}
  1. Prevent Permanent Locking of Staked Tokens

Ensure staking logic doesn't allow permanent token locking.

Example Code:

solidityCopyfunction addStake(uint32 unstakeDelaySec) external payable onlyOwner {
    entryPoint.addStake{value: msg.value}(unstakeDelaySec);
}
  1. Restrict Non-EntryPoint Transaction Execution

Verify that wallets implement proper permission checks for non-EntryPoint executions.

Example Code:

solidityCopy    function execute(address dest, uint256 value, bytes calldata func) external {
        _requireFromEntryPointOrOwner();
        _call(dest, value, func);
    }
  1. Limit Wallet Storage Access

Ensure wallets only access storage fields associated with the sender.

  1. Verify Paymaster's Failure Handling Logic

Check that Paymasters correctly handle fees in case of execution failures.

  1. Ensure Secure Implementation of Modular Wallets

Verify safe management of wallet modules and secure data storage when using DELEGATECALL.

Conclusion

This checklist provides a foundation for auditing account abstraction wallets based on the current EIP4337 standard. Given the early stages of EIP4337 implementation and varying wallet designs, auditors should conduct thorough checks based on specific wallet implementations.

Last updated