AA Signature Audit Remediation
Overview
ERC-4337 smart accounts validate user operations with signatures. Unlike EOA transactions, the protocol does not enforce chain ID and account binding for you, so a signature scheme that re-derives its own hash can be replayed across chains or across accounts that share an owner. The fix is to verify against the userOpHash the EntryPoint already provides, which commits to the EntryPoint address, chain ID, and every operation field.
Related Detector: AA Signature Audit
Recommended Fix
Before (Vulnerable)
function validateUserOp(PackedUserOperation calldata op, bytes32 userOpHash, uint256 funds)
external returns (uint256)
{
bytes32 hash = keccak256(abi.encode(op.sender, op.nonce, op.callData));
address signer = ECDSA.recover(hash, op.signature); // Missing chain ID!
return signer == owner ? 0 : 1;
}
After (Fixed)
function validateUserOp(PackedUserOperation calldata op, bytes32 userOpHash, uint256 funds)
external returns (uint256)
{
// userOpHash already includes EntryPoint address, chain ID, and all op fields
address signer = ECDSA.recover(
MessageHashUtils.toEthSignedMessageHash(userOpHash),
op.signature
);
return signer == owner ? 0 : 1;
}
The userOpHash is computed by the EntryPoint as a commitment over the operation, the EntryPoint address, and the chain ID. Recovering against it ties each signature to one operation on one account on one chain, so it cannot be replayed elsewhere.
Alternative Mitigations
- Use EIP-712 typed data with a domain separator that includes
chainIdandverifyingContract(the account address) when a custom signing scheme is required. - Recover with a library that rejects malleable signatures (rejecting
svalues in the upper half of the curve order), as OpenZeppelin’s ECDSA does. - For deliberately multi-chain accounts, document the cross-chain replay surface and require an explicit per-chain nonce in the signed payload.
Common Mistakes
- Re-deriving the hash from a subset of operation fields, dropping the chain ID and account binding.
- Using raw
ecrecoverwithout checking the return value foraddress(0)or guarding against malleability. - Assuming the EntryPoint-managed nonce protects a custom validation path that never reads it.