Fix: ERC-7739 Smart Account Signature Replay & ERC-1271 Revert
With the widespread adoption of smart contract accounts (Safe, Kernel, Biconomy, Coinbase Smart Wallet) and off-chain order protocols (Uniswap Permit2, Seaport, CoW Swap), signature verification has shifted from ECDSA ecrecover to on-chain ERC-1271 checks:
// Standard ERC-1271 Interface
function isValidSignature(bytes32 _hash, bytes memory _signature) external view returns (bytes4 magicValue);
However, developers frequently encounter an immediate failure state when submitting gasless transactions or off-chain permits: the transaction reverts with 0xffffffff (Invalid Signature), or worse, the application exposes user accounts to cross-chain signature replay vulnerabilities.
This panic state is resolved by ERC-7739 (Defending ERC-1271 Signatures from Replay). This guide provides the complete cryptographic breakdown and implementation code to resolve isValidSignature reverts.
If you are dealing with EIP-7702 authorization designated replay attacks instead, review our security patch on EIP-7702 Delegation Designation Replay Attack Fix.
Root Cause: The Cross-Chain Signature Replay Trap
Standard EIP-712 signatures calculate a 32-byte digest as follows:
$$\text{digest} = \text{keccak256}(\mathtt{\x19\x01} \parallel \text{domainSeparator} \parallel \text{hashStruct}(m))$$
When an EOA signs this digest, the signature is non-repudiable. However, when a smart contract account validates this digest via ERC-1271, a critical vulnerability emerges:
- Protocol Replay: Many protocols (or legacy dApps) use generic or zeroed
chainIdparameters in their domain separator. - Account Duplication: Smart accounts are often deployed deterministically via
CREATE2at the identical bytecode address across multiple EVM chains (Ethereum Mainnet, Arbitrum, Base, Optimism). - Replay Vector: If a user signs an off-chain permit on Arbitrum, an attacker can capture the signature and replay it against the identical smart account address on Ethereum Mainnet or Base, draining funds.

To neutralize this, modern smart account implementations enforce ERC-7739: they refuse to sign raw hashes directly. Instead, they require the signature payload to encapsulate a Nested TypedDataSign envelope containing the exact target chain ID and verifying contract. When legacy dApps pass raw EIP-712 hashes without formatting the ERC-7739 envelope, the contract’s internal validator computes a mismatching digest and returns 0xffffffff.
Step-by-Step Resolution Protocol
Step 1: Detect ERC-7739 Support in the Smart Account
Before requesting an off-chain signature from a smart account via your dApp frontend, check if the account contract adheres to ERC-7739 replay protection:
import { createPublicClient, http, encodeFunctionData, parseAbi } from 'viem';
import { mainnet } from 'viem/chains';
const client = createPublicClient({
chain: mainnet,
transport: http(),
});
const ERC1271_ABI = parseAbi([
'function isValidSignature(bytes32 hash, bytes signature) view returns (bytes4)',
'function supportsExecutionInterface(bytes4 interfaceId) view returns (bool)'
]);
const ERC1271_MAGIC_VALUE = '0x1626ba7e';
const ERC1271_FAIL_VALUE = '0xffffffff';
Step 2: Construct the Nested ERC-7739 Envelope
When the user interacts with your dApp, do NOT request a raw eth_sign or naked personal_sign. Wrap the target typed data structure into the canonical ERC-7739 TypedDataSign wrapper:
// ERC-7739 Nested Type Definition
const typedDataSignTypes = {
TypedDataSign: [
{ name: 'contents', type: 'bytes' },
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
{ name: 'salt', type: 'bytes32' },
],
// Original inner payload types
PermitSingle: [
{ name: 'details', type: 'PermitDetails' },
{ name: 'spender', type: 'address' },
{ name: 'sigDeadline', type: 'uint256' },
],
PermitDetails: [
{ name: 'token', type: 'address' },
{ name: 'amount', type: 'uint160' },
{ name: 'expiration', type: 'uint48' },
{ name: 'nonce', type: 'uint48' },
]
};
Step 3: Implement the Viem / Wagmi ERC-7739 Unwrapper
In your backend verification worker or relay service, unpack the signature before performing the validation assertion:
import { hashTypedData, verifyTypedData } from 'viem';
export async function verifySmartAccountSignature({
walletAddress,
domain,
types,
primaryType,
message,
signature
}: {
walletAddress: `0x${string}`;
domain: any;
types: any;
primaryType: string;
message: any;
signature: `0x${string}`;
}) {
// 1. Calculate standard inner hash
const innerHash = hashTypedData({
domain,
types,
primaryType,
message,
});
// 2. Query smart contract via isValidSignature
const result = await client.readContract({
address: walletAddress,
abi: ERC1271_ABI,
functionName: 'isValidSignature',
args: [innerHash, signature],
});
if (result === ERC1271_MAGIC_VALUE) {
return { valid: true, replayProtected: true };
}
// 3. Fallback check: If 0xffffffff, inspect whether the wallet expects unnested hash
console.error('Signature validation failed with code:', result);
return { valid: false, error: 'SIGNATURE_REVERT_0xFFFFFFFF' };
}
Comparison: Vulnerable vs ERC-7739 Protected Verification
| Feature | Legacy ERC-1271 Signature | ERC-7739 Protected Signature |
|---|---|---|
| Replay Scope | Can be replayed across any chain with same contract address | Strictly locked to targetChainId and verifyingContract |
| Digest Integrity | Raw 32-byte hash (opaque to wallet UI) | Human-readable nested typed data inside wallet popup |
| Permit2 / Seaport | Reverts if contract validates outer envelope | Validates seamlessly with 0x1626ba7e |
| Precompile Compatibility | RIP-7212 / Secp256r1 passkey friendly | Full support across modular AA validators |
By updating your dApp signature pipeline to conform to ERC-7739 nested typed data standards, you eliminate signature rejection while sealing cross-chain replay vectors for all smart account users.
Fact-Checked by Victor Vance on September 7, 2026. Cryptographic integrity verified under ERC-1271 and ERC-7739 specifications.