UserOperation Signature

The signature field on a UserOperation encodes an arbitrary signature to validate that the transaction is legit.

All User Operation signatures should be a signed message of the userOpHash. This userOpHash is a keccak256 hash of:

  1. The entire User Operation (minus the signature field).
  2. The EntryPoint address.
  3. The chain ID.

To optimize gas, the EntryPoint will generate this hash once and pass it to both validateUserOp and validatePaymasterUserOp during the verification phase.

Example: generating a userOpHash

const packedData = ethers.utils.defaultAbiCoder.encode(
  [
    "address",
    "uint256",
    "bytes32",
    "bytes32",
    "uint256",
    "uint256",
    "uint256",
    "uint256",
    "uint256",
    "bytes32",
  ],
  [
    sender,
    nonce,
    ethers.utils.keccak256(initCode),
    ethers.utils.keccak256(callData),
    callGasLimit,
    verificationGasLimit,
    preVerificationGas,
    maxFeePerGas,
    maxPriorityFeePerGas,
    ethers.utils.keccak256(paymasterAndData),
  ]
);

const enc = ethers.utils.defaultAbiCoder.encode(
  ["bytes32", "address", "uint256"],
  [ethers.utils.keccak256(packedData), entryPoint, chainId]
);

const userOpHash = ethers.utils.keccak256(enc);
// See the EntryPoint contract at
// https://github.com/eth-infinitism/account-abstraction/blob/abff2aca61a8f0934e533d0d352978055fddbd96/contracts/core/EntryPoint.sol#L267-L269

function getUserOpHash(
UserOperation calldata userOp
) public view returns (bytes32) {
  return
  keccak256(abi.encode(userOp.hash(), address(this), block.chainid));
}
// See the Stackup bundler code at
// https://github.com/stackup-wallet/stackup-bundler/blob/34cd6b4ebc7a8c0b6a135f947f68911fd475c33a/pkg/userop/object.go#L178-L184

func (op *UserOperation) GetUserOpHash(entryPoint common.Address, chainID *big.Int) common.Hash {
	return crypto.Keccak256Hash(
		crypto.Keccak256(op.PackForSignature()),
		common.LeftPadBytes(entryPoint.Bytes(), 32),
		common.LeftPadBytes(chainID.Bytes(), 32),
	)
}

The User Operation signature

ERC-4337 is agnostic to any specific signature scheme. This means users can validate with anything from ECDSA, multi-sig, or WebAuthn to name a few examples. No matter the scheme used, the end result should be a signature of the userOpHash for maximum security. Once a signature of the userOpHash is generated, no field can be changed without going through the signing process again.

In the case of a SimpleAccount.sol, the signature is required to be signed by a trusted EOA owner. However the signer in the example below could just as well be an abstraction for a biometric device and verified according to that WebAuthn standard during validateUserOp.

const signature = signer.signMessage(
  ethers.utils.arrayify(userOpHash)
);