It seems that signing EIP-4337 user operations is underspecified, and this has led to most implementations leveraging personalSign (starting with the reference implementation). Using personalSign is not ideal, for a couple of reasons.
- personalSign was introduced so that users could have confidence they weren’t signing a transaction.
That means that any
signed_data
cannot be one RLP-structure, but a 1-byteRLP
payload followed by something else. Thus, any EIP-191signed_data
can never be an Ethereum transaction.
As user operations are effectively transactions, which can spend the users fund, it seems we have taken a step backwards. Users can be tricked into signing a user operation that steals all their funds. Wallets do not show warnings on personalSign, even when signing 32 bytes which could be a transaction hash, because they presume the prefix makes it safe for users. For user operations, it is not safe.
- Using personalSign makes it near impossible for wallets to tell users what they are signing. Seemingly, this hasn’t been a huge issue to date because generally either
- Dapps leveraging 4337 have embedded wallets, and so they dapp itself is the wallet and can tell the user whatever info about what they are signing.
- The wallet is receiving eth_sendTransaction requests and turning them into user operations, so the user’s wallet can show the user all they need to know about the transaction.
This all makes sense, but constrains how 4337 can be used. For example, I may have a wallet that leverages 4337 and be interacting with a dapp that does, as well. Suppose the dapp has its own paymaster, which my wallet knows nothing about, and wants to include the paymaster and signature in the request sent to the wallet. Perhaps the dapp wants to set some of the user operation gas values, and help the wallet with things it may not be aware of. Currently there is no way to do these things.
Beyond this, we can also say there is currently no good way for existing EOA wallets to serve as a signer for 4337 accounts, because the EOA wallet cannot show any helpful info when signing. Signing 32 bytes of hex via personalSign should not be acceptable.
I propose a new RPC, eth_signUserOperation
inputs: {
userOperation: UserOperation,
entryPoint: address,
chainId: bytes,
}
output: {signature: bytes}
We can also use this opportunity to specify how exactly the user operation should be hashed, as the industry seems to have a convention that is not in the EIP.
userOpHash = keccak256(abi.encode(hashedUserOperation, entryPoint, chainId))
where
hashedUserOperation = (
abi.encode(
sender,
nonce,
keccak256(initCode),
keccak256(callData),
callGasLimit,
verificationGasLimit,
preVerificationGas,
maxFeePerGas,
maxPriorityFeePerGas,
keccak256(paymasterAndData),
)
)
Thanks for reading, and I welcome feedback. If this makes sense to others, and I am not missing something obvious, we can turn this into a formal EIP.