Smart Account Signatures
A smart account — a Safe multisig, an Argent wallet, an ERC-4337 contract account, a DAO treasury — cannot sign messages the way an Externally Owned Account (EOA) can. It has no private key, so ecrecover cannot recover “its” address from a signature. Without a standard bridge, smart accounts are second-class citizens on the signature layer: every dapp that accepts off-chain signatures would implicitly require an EOA signer, and contract-owned identities would be locked out of everything from DEX orders to gasless approvals to social logins. EIP-1271 is that bridge.
The gap EIP-1271 closes
| EOA signer | Smart contract signer | |
|---|---|---|
| How it signs | Wallet computes ECDSA (r, s, v) over the digest using the private key | There is no private key — the contract “signs” by acknowledging the digest through whatever logic it implements |
| How verifiers check | Call ecrecover(digest, v, r, s) and compare the recovered address to the expected signer | Call expectedSigner.isValidSignature(digest, signature) and compare the return to 0x1626ba7e |
| What the signature bytes contain | Exactly 65 bytes of ECDSA | Opaque bytes interpreted by the signing contract — could be multisig concat, a BLS aggregation, or even empty bytes |
| Validation logic | Fixed: ECDSA over secp256k1 with EIP-2 low-s constraint | Arbitrary: whatever the signing contract implements — owner sets, thresholds, roles, nonces, deadlines, pre-approved hashes |
| State dependency | None — ecrecover is pure | Potentially any read-only state: owner sets, role mappings, storage lookups |
| Gas cost to verify | ~3,000 gas (one ecrecover precompile call) | Unbounded — depends on what the signing contract does |
EIP-1271 does not try to make smart accounts sign “like EOAs”. It accepts that they can’t, and instead standardizes the verification interface so both paths produce the same yes/no answer.
The verifier’s branching logic
A verifier that wants to accept both EOA and smart-account signers does this:
function verify(address signer, bytes32 hash, bytes calldata signature) internal view returns (bool) {
if (signer.code.length == 0) {
// EOA path
return ECDSA.recover(hash, signature) == signer;
} else {
// Contract path
(bool ok, bytes memory result) = signer.staticcall(
abi.encodeCall(IERC1271.isValidSignature, (hash, signature))
);
return ok && result.length >= 32 && abi.decode(result, (bytes4)) == 0x1626ba7e;
}
}
OpenZeppelin’s SignatureChecker.isValidSignatureNow(signer, hash, signature) is exactly this pattern, with a couple of extra safeguards (e.g. checking that the return data is exactly 32 bytes before decoding).
The branching condition is signer.code.length > 0 — in other words, “does this address have contract code?”. EOAs have no code; contracts do. The check is not perfectly future-proof (ERC-4337 proposes ways for addresses to temporarily be treated as contracts before their code is deployed), but it is the convention every current verifier uses.
What smart accounts actually do inside isValidSignature
Different smart-account designs implement the function in very different ways:
Safe (Gnosis) — two modes:
- Non-empty signature: parse as concatenated owner signatures, run
checkSignaturesagainst the current owner set and threshold. - Empty signature: look the hash up in the
signedMessagesstorage mapping, which is populated by prior on-chainSignMessageLib.signMessagedelegatecalls. See Creating a Farcaster Account by Hand for the empty-signature mode in action.
Argent / Ambire / Sequence — typically single-signer ECDSA against a stored owner, with guardian overrides and recovery logic layered on top.
ERC-4337 account abstraction — the validateUserOp function on the account implementation effectively performs an EIP-1271 check on the UserOperation hash. The account decides what “valid” means: session keys, spending limits, 2FA via a second signer, time windows, off-chain attestation proofs, etc.
DAOs — “valid” means the governance process passed a proposal authorizing this specific digest. The isValidSignature function reads the proposal state and returns the magic value iff a matching proposal is in the executed state.
Custody services — multisig with policy engines that gate signatures on whitelist checks, travel-rule compliance, or off-chain approvals.
From the verifier’s point of view, all of these look identical: isValidSignature(hash, signature) returns the magic value iff the signature is valid. The complexity is hidden behind the interface.
What the signer side must agree on out of band
EIP-1271 standardizes the interface, not the semantics. The verifier and the signing contract must separately agree on:
- Hashing scheme: Raw
keccak256, Ethereum Signed Message, EIP-712 with a specific domain — in modern Ethereum, EIP-712 is the default. - Signature format: ECDSA bytes, multisig concatenation, empty-bytes pre-approval, custom blob.
- Nonce management: Who tracks replay protection, and whether the nonce is inside the signature or inside the contract state.
- Deadline: Whether the digest has a deadline baked in, and who enforces it.
When an application like OpenSea or Farcaster integrates smart-account signers, it typically just calls SignatureChecker.isValidSignatureNow with an EIP-712 digest and opaque signature bytes and trusts the signing contract to know what to do. The application doesn’t need to understand Safe’s SafeMessage wrapping or Argent’s guardian logic — it just asks the question and gets the answer.
Pre-approved hashes as on-chain signatures
A smart account can substitute an on-chain transaction for a cryptographic signature. Safe’s SignMessageLib.signMessage(data) pattern is the canonical example:
- The Safe executes a delegatecall to
SignMessageLib, which writessignedMessages[msgHash] = 1into the Safe’s own storage. - Later, when a verifier calls
Safe.isValidSignature(digest, "")with empty signature bytes, Safe’s fallback handler checkssignedMessages[wrappedDigest]and returns the magic value if it’s set.
The on-chain transaction is the signature. There’s no cryptographic artifact — just an attestation stored in the contract’s state. This is equivalent to a traditional signature in every way that matters for EIP-1271, and it is essential when the verifier cannot interact with the signer live (e.g. when Farcaster’s validator calls back into a Safe after the initial transaction).
The trade-off is one extra on-chain transaction in exchange for “no private keys required and works with any Safe topology”. For high-value flows where the Safe is the custody layer from day zero, this is exactly the right trade.
What this enables
EIP-1271 is why every modern smart account can participate in the Ethereum signature economy:
- Safe multisigs can buy NFTs on OpenSea — OpenSea verifies the buyer’s signed order via
isValidSignatureif the buyer is a contract. - ERC-4337 accounts can use gasless token approvals —
permitvia EIP-1271 instead of EIP-2612’s direct ECDSA path. - DAOs can sign into dapps — Sign-In with Ethereum (EIP-4361) verifies contract signers via EIP-1271.
- Smart wallets can route Farcaster signatures through pre-approval — see the Farcaster walkthrough for the full flow.
Without EIP-1271, all of these would require the application to handle smart accounts as special cases (or not support them at all). With EIP-1271, the application writes one verification path and smart accounts show up as drop-in replacements for EOAs.
Connections
- ERC-1271: Standard Signature Validation Method for Contracts: the standard this page depends on
- EIP-1271 isValidSignature: the interface method in detail
- Off-Chain Signatures: the broader pattern smart accounts need to participate in
- EIP-712: Typed structured data hashing and signing: the hashing standard smart accounts almost always use
- Creating a Farcaster Account by Hand: applied example of Safe + EIP-1271 + pre-approved hash