EIP-4337: Account Abstraction Security Checklist
Security concerns and best practices for developers when implementing EIP-4337
Table Of Contents
Overview
An account abstraction proposal that completely avoids the need for consensus-layer protocol changes.
Instead of adding new protocol features and changing the bottom-layer transaction type, this proposal introduces a higher-layer pseudo-transaction object called a UserOperation.
Users send UserOperation objects into a separate mempool.
A special class of actors called bundlers package up a set of these objects into a transaction making a handleOps call to a special contract, and that transaction then gets included in a block.
Concepts
UserOperation: a structure that describes a transaction to be sent on behalf of a user. To avoid confusion, it is not named “transaction”.
Sender: the account contract sending a user operation.
EntryPoint: a singleton contract to execute bundles of UserOperations
Bundler: a node (block builder) that can handle UserOperations, create a valid
EntryPoint.handleOps()transaction and add it to the block while it is still valid.Aggregator: a helper contract trusted by accounts to validate an aggregated signature
Workflow
Security Concerns - Checklist
Bundler
Clients must whitelist the supported entrypoint
clients must whitelist the supported aggregators
Reading and validating the nonce of UserOps
If using a Classic Sequential Nonce:
require(userOp.nonce < type(uint64).max)If support operations running in parallel, Ordered Administrative Events:
bytes4 sig = bytes4(userOp.callData[0 : 4]);
uint key = userOp.nonce >> 64;
if (sig == ADMIN_METHODSIG) {
require(key == ADMIN_KEY, "wrong nonce-key for admin operation");
} else {
require(key == 0, "wrong nonce-key for normal operation");
}
an account uses signature aggregation when it returns its address from validateUserOp. Or return ValidationResultWithAggregator in simulateValidation
The bundler should first accept the aggregator (validate its stake info and that it is not throttled/banned)
Then it MUST verify the userOp calling
aggregator.validateUserOpSignature()
Entrypoint
Entrypoint:innerHandleOp, validate gasLimit to leave more than callGasLimit to call: are there implemented INNER_OUT_OF_GAS in Entrypoint.sol (ref: line 212) and handled them in a try-catch block while calling this.innerHandleOp (ref: line 68). Ref: merge commitEntrypoint:handleOps:innerHandleOpmust try catch and verify remaining gas is more than sufficient to cover the specified mUserOp.callGasLimit and mUserOp.verificationGasLimit. Ref: issue1Entrypoint:handleAggregatedOps, handleOpsFollowing Required entry point contract functionality at https://eips.ethereum.org/EIPS/eip-4337#specification
Ref: issue
Entrypoint:handleOps-> verification loop: if paymaster is not zero address (supported)Check that the paymaster has enough ETH deposited
Then call
validatePaymasterUserOpon the paymaster to verify that the paymaster is willing to pay for the operationIf the
validatePaymasterUserOpreturns a “context”, thenhandleOpsmust callpostOpon the paymaster after making the main execution call.
Paymaster (Optional)
PaymasterMaliciously crafted paymasters can DoS the system. To prevent this, we use a reputation system. paymaster must either limit its storage usage or have a stakePaymaster:getHashUserOperation hashing performed incorrectly: To prevent replay attacks (both cross-chain and multipleEntryPointimplementations), thesignatureshould depend onchainidand theEntryPointaddress.
// Recommend correctly hashing
function getHash(UserOperation calldata userOp) public pure returns (bytes32) {
keccak256(abi.encode(
userOp.getSender(),
userOp.nonce,
keccak256(userOp.initCode),
keccak256(userOp.callData),
userOp.callGasLimit,
userOp.verificationGasLimit,
userOp.preVerificationGas,
userOp.maxFeePerGas,
userOp.maxPriorityFeePerGas
block.chainid
));
}
PaymasterThe paymaster must also have a deposit, which the entry point will charge UserOperation costs from. The deposit (for paying gas fees) is separate from the stake (which is locked).The paymaster implements the right interface:
function validatePaymasterUserOp
(UserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost)
external returns (bytes memory context, uint256 validationData);
function postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost) external;
enum PostOpMode {
opSucceeded, // user op succeeded
opReverted, // user op reverted. still has to pay for gas.
postOpReverted // user op succeeded, but caused postOp to revert
}
function addStake(uint32 _unstakeDelaySec) external payable
function unlockStake() external
function withdrawStake(address payable withdrawAddress) external
The entrypoint implements the interface to allow paymaster to manage their deposit:
function balanceOf(address account) public view returns (uint256)
function depositTo(address account) public payable
function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) externalAccount
Account:validateUserOp: TheuserOpHashmust be a hash over all fields of userOp (except a signature) + entrypoint + chainId. Ref: missing chainId[
Account:validateUserOp: If the account does not support signature aggregation, it MUST validate the signature is a valid signature of theuserOpHash, and SHOULD return SIG_VALIDATION_FAILED (and not revert) on signature mismatch. Any other error should revert.Account:validateUserOp: MUST pay the entryPoint (caller) at least the “missingAccountFunds” (might be zero). Pay more to cover future transactions (withdrawTo)Account:validateUserOp: The return value MUST be packed ofauthorizer,validUntilandvalidAftertimestamps.
Aggregator
Aggregator:aggregateSignatures()must aggregate all UserOp signatures into a single value.Aggregator:validateSignatures()MUST validate the aggregated signature matches for all UserOperations in the array, and revert otherwise


