LayerZeroFault
ai agents-api Updated:

Viem EIP-712 Nested Struct Hash Mismatch Revert Fix

VV

Written by

Updated:

Technical Guide

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 keccak256 of the concatenated encodeData of 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 digest on-chain and compare it character-by-character with Viem’s hashTypedData return value.
  • Verify chainId Type: Ensure chainId in the domain is passed as an unsigned integer/bigint, not a hex string or decimal string.
  • Check ERC-5267 eip712Domain(): Call eip712Domain() 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 / PlatformSubsystemRoot CauseResolution Guide
0x6a80 (Ledger)Arbitrum / EVM AppNested struct depth or blind signing disabledLedger EthAppCommandError 6a80 Fix
0x6985 (Trezor)Firmware Clear SigningStruct member overflow or rejected by userTrezor EIP-712 0x6985 Firmware Fix
Blind Signing RevertLedger Live Web3Blind signing turned OFF after app updateLedger Live EIP-712 Blind Signing Revert
Multicall Auto-ResetFirmware SessionFirmware resets blind signing on power cycleLedger Auto-Reset Blind Signing Multicall Fix
Type Hash MismatchViem / TypeScriptNested custom struct alphabetization bugViem EIP-712 Nested Types Hash Mismatch
ERC-7739 ReplaySmart Account / ERC-1271Cross-account signature replay on Permit2ERC-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/.

Interactive Diagnostic Sandbox

Local Agent API & Calldata Sandboxes

Inspect autonomous agent UserOperations, decode packed execution calldata, and test programmatic authentication payloads locally.

Companion Diagnostic Guide Viem shouldRetry QuickNode -32007 Rate-Limit Error Fix →

Fix unhandled JSON-RPC error -32007 in Viem with QuickNode RPCs. Custom retry handlers for rate-limit codes prevent agent execution loop crashes.

Read Guide
Contextual Architecture

Related Diagnostic Guides

View All in Silo →

Related Inquiries

Why does my EIP-712 signature revert on-chain when Viem signs it successfully?

Under EIP-712, if the types object in signTypedData defines nested structs in a different order or omits sub-type dependencies, the derived typeHash mismatches Solidity's keccak256(abi.encodePacked(...)) representation, producing an invalid signature digest.

How does primaryType affect EIP-712 hashing in Viem?

The primaryType declares the root struct for the signature. Viem dynamically discovers dependent sub-types from this root. If primaryType does not match the topmost struct, typeHash generation deviates from the contract.

How do I debug EIP-712 domain separator mismatches?

Verify that verifyingContract, chainId, name, and version in the domain object match the on-chain contract's DOMAIN_SEPARATOR hash precisely. Differences in chainId on L2 rollups are a frequent cause of invalid signature errors.

How can I decode and inspect on-chain calldata for EIP-712 transactions?

Use our interactive ERC-4337 Calldata Decoder tool to inspect function selectors, ABI-encoded parameters, and signature bytes offline.