Universal signature verification: EOA (ecrecover), smart-contract wallets
(ERC-1271) and counterfactual —
not-yet-deployed — wallets (ERC-6492).
Smart-contract wallets (Safe, Coinbase Smart Wallet, ERC-4337/EIP-7702 accounts, ...)
cannot produce signatures that ecrecover validates. Backends which only use
ecrecover-based verification (Ethers.PersonalMessage.verify/3,
Ethers.TypedData.valid_signature?/3) silently reject all smart-wallet users. The
functions in this module verify any signature:
- Fast path — for plain 65-byte ECDSA signatures,
ecrecoverruns locally first. A match verifies without any RPC round-trip. - Universal path — otherwise, one
eth_callexecutes theEthers.Contracts.UniversalSigValidatorcontract deploylessly (notoaddress, nothing gets deployed on chain). The validator unwraps ERC-6492 signatures (counterfactually deploying the wallet inside the call), performs the ERC-1271isValidSignature/2check for contract accounts, and falls back toecrecover.
Verification outcome is a boolean inside an ok-tuple: {:ok, false} means the
signature is invalid, while {:error, reason} is reserved for transport/RPC
failures. There are deliberately no bang variants — raising would conflate an invalid
signature with an RPC failure.
Example
A backend verifying a wallet login message:
{:ok, true} = Ethers.Signature.verify_message("Sign in to Example", signature, address)
# With explicit RPC options
Ethers.Signature.verify_message("Sign in to Example", signature, address,
rpc_opts: [url: "https://eth.example.com"]
)
Summary
Functions
@spec verify_hash(binary(), binary(), Ethers.Types.t_address(), Keyword.t()) :: {:ok, boolean()} | {:error, term()}
Verifies a signature over a 32-byte digest against an address.
Accepts any signature kind: plain 65-byte ECDSA (EOA), ERC-1271 (verified on-chain
via the account contract) and ERC-6492-wrapped (counterfactual wallets). Plain EOA
signatures that recover to address are verified locally without any RPC call.
Parameters
hash: The 32-byte digest that was signed — either a raw binary or a0x-prefixed hex string. (e.g.Ethers.PersonalMessage.hash/1orEthers.TypedData.hash/1)signature: The signature as a0x-prefixed hex string or raw binary. ERC-6492-wrapped signatures (longer than 65 bytes) are passed to the validator untouched.address: The address to verify against (EOA or smart-contract account).opts: Options.
Options
:block: The block number (integer) or block tag for the validationeth_call. Defaults to"latest".:rpc_client: The RPC Client to use. It should implement ethereum jsonRPC API. default: Ethereumex.HttpClient:rpc_opts: Extra options to pass to rpc_client. (Like timeout, Server URL, etc.)
Returns
{:ok, true}if the signature is valid foraddress.{:ok, false}if the signature is invalid.{:error, reason}on malformed input or RPC transport failure.
@spec verify_message(binary(), binary(), Ethers.Types.t_address(), Keyword.t()) :: {:ok, boolean()} | {:error, term()}
Verifies a signature over an EIP-191
personal message (the personal_sign scheme) against an address.
Hashes message with Ethers.PersonalMessage.hash/1 and delegates to
verify_hash/4 — see it for accepted signature kinds, options and return values.
@spec verify_typed_data( Ethers.TypedData.t(), binary(), Ethers.Types.t_address(), Keyword.t() ) :: {:ok, boolean()} | {:error, term()}
Verifies a signature over EIP-712 typed structured data against an address.
Hashes typed_data with Ethers.TypedData.hash/1 and delegates to verify_hash/4 —
see it for accepted signature kinds, options and return values.