The smart contract verification error ECDSA: invalid signature or ERC1271: signature invalid when verifying EIP-712 typed data in Viem and Wagmi occurs because nested struct types in the types object are misordered or omit referenced custom types. Under EIP-712, struct typehashes are computed by sorting referenced sub-types alphabetically. If the primaryType or nested structs omit sub-type declarations, the generated digest mismatches on-chain verification.
To unpack function selectors and verify signature parameters offline, paste your transaction data into our ERC-4337 Calldata Decoder Tool. For RPC connection stability, see our companion guide on Viem QuickNode -32007 rate-limit retries.
The Cryptographic Mechanics of EIP-712 Digest Construction
Under EIP-712, the final 32-byte signing digest is constructed as:
$$\text{digest} = \text{keccak256}\left(\texttt{“\x19\x01”} ,|, \text{domainSeparator} ,|, \text{hashStruct}(\text{primaryType}, \text{data})\right)$$
The core discrepancy virtually always originates inside $\text{hashStruct}$, which is defined as:
$$\text{hashStruct}(s) = \text{keccak256}\left(\text{typeHash} ,|, \text{encodeData}(s)\right)$$
Pitfall 1: Type String Alphabetical Ordering Rules
EIP-712 mandates that the typeHash begins with the primary type definition, followed by any referenced sub-types sorted strictly in alphabetical order by their type name:
Consider a contract with these structs:
struct Route {
address router;
Hop[] hops;
FeeConfig fees;
}
struct Hop {
address pool;
uint24 fee;
}
struct FeeConfig {
address recipient;
uint256 bps;
}
The canonical type string for Route MUST place FeeConfig before Hop because "F" precedes "H" alphabetically:
$$\texttt{“Route(address router,Hop[] hops,FeeConfig fees)FeeConfig(address recipient,uint256 bps)Hop(address pool,uint24 fee)”}$$
If a Solidity developer manually defines their constant ROUTE_TYPEHASH in the order fields appear in the struct:
// BUGGY ON-CHAIN DEFINITION: Hop placed before FeeConfig!
bytes32 constant ROUTE_TYPEHASH = keccak256(
"Route(address router,Hop[] hops,FeeConfig fees)Hop(address pool,uint24 fee)FeeConfig(address recipient,uint256 bps)"
);
Viem’s automated hashTypedData internal parser strictly obeys the official EIP-712 specification, generating the alphabetized hash (FeeConfig first). As a result, the off-chain and on-chain typeHash constants diverge completely:
Solidity on-chain: 0xa4b19c8f... (Unhashed order)
Viem client-side: 0x3e88d120... (Canonical EIP-712 alphabetized order)
Result: keccak256 digest mismatch → ecrecover yields wrong signer address!
Pitfall 2: Dynamic Arrays of Structs (Item[])
When a struct contains a dynamic array of another struct (such as Order(address maker,Item[] items)):
Under EIP-712 Section Definition of encodeData:
The array values are encoded as the
keccak256of the concatenatedencodeDataof their contents.
The Common Solidity Encoding Mistake
Developers frequently attempt to hash the array using standard ABI encoding:
// INCORRECT SOLIDITY ENCODING:
bytes32 structHash = keccak256(
abi.encode(
ORDER_TYPEHASH,
order.maker,
keccak256(abi.encode(order.items)) // ← FATAL ERROR!
)
);
abi.encode(order.items) encodes EVM memory offsets and head-tail dynamic array lengths. In contrast, EIP-712 mandates an array of 32-byte hashes:
$$\text{encodedArray} = \text{keccak256}\left(\text{hashStruct}(\text{item}_0) ,|, \text{hashStruct}(\text{item}1) ,|, \dots ,|, \text{hashStruct}(\text{item}{n-1})\right)$$
Correct On-Chain Solidity Implementation
function hashOrder(Order memory order) internal pure returns (bytes32) {
bytes32[] memory itemHashes = new bytes32[](order.items.length);
for (uint256 i = 0; i < order.items.length; i++) {
itemHashes[i] = keccak256(
abi.encode(
ITEM_TYPEHASH,
order.items[i].token,
order.items[i].amount
)
);
}
return keccak256(
abi.encode(
ORDER_TYPEHASH,
order.maker,
keccak256(abi.encodePacked(itemHashes)) // ← Correctly hashes concatenated 32-byte words
)
);
}
Pitfall 3: Viem Client-Side Type Definitions
In Viem, if you define your types object using as const, ensure that you do NOT define the EIP712Domain key inside types. Viem automatically constructs the EIP712Domain type based on the keys present in the domain parameter.
import { hashTypedData } from 'viem';
// CORRECT VIEM CONFIGURATION
const types = {
// Do NOT include EIP712Domain here!
Hop: [
{ name: 'pool', type: 'address' },
{ name: 'fee', type: 'uint24' },
],
FeeConfig: [
{ name: 'recipient', type: 'address' },
{ name: 'bps', type: 'uint256' },
],
Route: [
{ name: 'router', type: 'address' },
{ name: 'hops', type: 'Hop[]' },
{ name: 'fees', type: 'FeeConfig' },
],
} as const;
export function computeOffchainDigest(route: any, chainId: number, verifyingContract: `0x${string}`) {
return hashTypedData({
domain: {
name: 'LayerZeroFault Intent Engine',
version: '1',
chainId: BigInt(chainId),
verifyingContract,
},
types,
primaryType: 'Route',
message: route,
});
}
Defensive Verification Checklist
- Print Both Digests: In test environments or Hardhat/Foundry tests, emit an event or console log
digeston-chain and compare it character-by-character with Viem’shashTypedDatareturn value. - Verify
chainIdType: EnsurechainIdin the domain is passed as an unsigned integer/bigint, not a hex string or decimal string. - Check ERC-5267
eip712Domain(): Calleip712Domain()on the verifying contract to confirm that the deployed contract is using the expected name, version, and salt parameters.
Fact-Checked by Victor Vance, Senior Smart Contract Security Analyst & Node Operator.
EIP-712 Signature & Hardware Signing Reference Matrix
Cross-reference EIP-712 typed data signing failures, hardware wallet firmware errors, and smart contract verification codes:
| Failure Code / Platform | Subsystem | Root Cause | Resolution Guide |
|---|---|---|---|
| 0x6a80 (Ledger) | Arbitrum / EVM App | Nested struct depth or blind signing disabled | Ledger EthAppCommandError 6a80 Fix |
| 0x6985 (Trezor) | Firmware Clear Signing | Struct member overflow or rejected by user | Trezor EIP-712 0x6985 Firmware Fix |
| Blind Signing Revert | Ledger Live Web3 | Blind signing turned OFF after app update | Ledger Live EIP-712 Blind Signing Revert |
| Multicall Auto-Reset | Firmware Session | Firmware resets blind signing on power cycle | Ledger Auto-Reset Blind Signing Multicall Fix |
| Type Hash Mismatch | Viem / TypeScript | Nested custom struct alphabetization bug | Viem EIP-712 Nested Types Hash Mismatch |
| ERC-7739 Replay | Smart Account / ERC-1271 | Cross-account signature replay on Permit2 | ERC-7739 Smart Account Signature Replay Fix |
[!TIP] Decode packed EIP-712 structs and inspect UserOperation hashes locally in our client-side /tools/erc4337-calldata-decoder/.