The Trezor hardware wallet error 0x6985 Action Cancelled or Failure_ActionCancelled during EIP-712 typed data signatures (such as Uniswap Permit2, CowSwap, or Blur orders) occurs when the firmware encounters nested structs exceeding buffer memory or when the browser WebUSB bridge drops connection before confirmation. Updating Trezor firmware, enabling verbose domain display, or using desktop bridges prevents premature 0x6985 aborts.
To inspect the underlying contract call and verify EIP-712 parameters offline, use our interactive ERC-4337 Calldata Decoder Tool. For companion hardware signing errors, check Ledger blind signing auto-reset troubleshooting and Ledger EthAppCommandError 0x6a80 fixes.
1. Technical Mechanics of EIP-712 Parsing on Trezor Core
Unlike raw transaction signing—where the hardware device only needs to decode recursive length prefix (RLP) bytes—EIP-712 Typed Data signing requires the hardware security enclave to construct and hash structured data in two separate passes:
$$\text{Sign}(\text{DomainSeparator}, \text{HashStruct}(P))$$
To prevent blind signing exploits, modern Trezor Core firmware (v2.6.0 and later) attempts to parse and visualize the entire tree structure on-device:
- Domain Separator Validation: Checks
name,version,chainId, andverifyingContract. - Schema Type Decomposition: Validates that all custom structs declared in
typesare strictly acyclic and conform to Ethereum ABI types (address,uint256,bytes32, etc.). - Recursive Value Serialization: Traverses each field and hashes dynamic members (strings and byte arrays) using Keccak-256.
If any field violates strict type constraints, the hardware parser considers the payload malformed and immediately aborts the session with protocol code Failure_ActionCancelled (0x6985).
2. Primary Root Causes
1. The chainId Type Mismatch (Hex String vs. BigInt / Number)
The EIP-712 specification states that chainId in EIP712Domain is a uint256. However, several decentralized applications (dApps) serialize chainId as a hexadecimal string (e.g., "0x01" or "0x2105" for Base) instead of a numeric value or plain decimal string (1 or 8453).
While software wallets like MetaMask silently coerce hex strings to BigInts, Trezor Core rejects string-encoded uint256 scalars in domain headers, causing an instantaneous 0x6985 abort.
2. Deeply Nested Struct Arrays Exceeding SRAM Buffer
Trezor Model T and Safe 3 hardware chips operate within tight static RAM constraints (approx. 128 KB - 256 KB total SRAM).
When signing batch trades or complex order routing payloads (e.g., UniswapX dutch orders containing arrays of 10+ filled input and output tokens with nested permits), the deserialization tree overflows Trezor’s temporary heap allocation. To prevent memory corruption or stack smashes, the firmware triggers a defensive abort.
3. WebUSB Chunk Desynchronization in Chromium
Under Chrome and Brave (versions 124+), WebUSB communication uses a 64-byte frame delimiter. If an extension initiates multiple concurrent RPC calls (for instance, fetching account balance while requesting an EIP-712 signature), the USB endpoint receives interleaved control packets, causing the Trezor bridge (trezord or WebUSB driver) to drop the signature packet and report Action cancelled.
3. Diagnostic & Troubleshooting Procedure
Diagnostic Test: Verify Payload with trezorctl
To isolate whether the issue stems from your browser extension or the typed payload itself, test the signature directly using the official trezorctl CLI utility:
# Save your EIP-712 JSON payload to eip712_payload.json
trezorctl ethereum sign-typed-data \
--address "m/44'/60'/0'/0/0" \
--chain-id 1 \
eip712_payload.json
If trezorctl outputs:
Error: Data error: Invalid field type for chainId (expected integer, received string)
The root cause is a malformed JSON schema submitted by the dApp.
4. Remediation & Recovery Protocols
Method A: Sanitize the EIP-712 Payload in Front-End Integration
If you are developing a dApp or integrating Viem / Ethers.js with Trezor Connect, sanitize your domain and message structures before passing them to the hardware provider:
import { type TypedDataDefinition } from 'viem';
export function sanitizeForTrezor<T extends TypedDataDefinition>(typedData: T): T {
const sanitized = JSON.parse(JSON.stringify(typedData));
// 1. Ensure domain chainId is an integer
if (sanitized.domain?.chainId) {
if (typeof sanitized.domain.chainId === 'string') {
sanitized.domain.chainId = sanitized.domain.chainId.startsWith('0x')
? parseInt(sanitized.domain.chainId, 16)
: parseInt(sanitized.domain.chainId, 10);
}
}
// 2. Strip unused struct definitions from types to conserve device SRAM
const referencedTypes = new Set<string>();
function findReferences(typeName: string) {
referencedTypes.add(typeName);
const fields = sanitized.types[typeName] || [];
for (const f of fields) {
const cleanType = f.type.replace('[]', '');
if (sanitized.types[cleanType] && !referencedTypes.has(cleanType)) {
findReferences(cleanType);
}
}
}
findReferences(sanitized.primaryType);
for (const typeKey of Object.keys(sanitized.types)) {
if (typeKey !== 'EIP712Domain' && !referencedTypes.has(typeKey)) {
delete sanitized.types[typeKey];
}
}
return sanitized;
}
Method B: Configure Trezor Suite & Trezor Bridge Settings
If signing as an end-user on a third-party dApp:
- Open Trezor Suite Desktop: Ensure your device is running the latest stable firmware (Model T $\ge$
2.7.2, Safe 3 $\ge$2.7.2). - Close Redundant Browser Tabs: Close background dApps or portfolio trackers querying your Trezor simultaneously via WebUSB.
- Switch to Trezor Bridge (Standalone Daemon):
- In Trezor Suite settings, enable Trezor Bridge (
http://127.0.0.1:21325/). - In your browser wallet (e.g., MetaMask Settings > Advanced), toggle the connection method from WebUSB to Trezor Bridge. Trezor Bridge handles USB packet framing natively in the OS kernel, preventing Chromium packet truncation.
- In Trezor Suite settings, enable Trezor Bridge (
Method C: Hardware Blind Signing Fallback
If the dApp uses ultra-complex nested struct arrays that inherently exceed device memory:
- Connect Trezor to Trezor Suite.
- Navigate to Settings > Device > Ethereum Safety Checks.
- Set the mode from Strict to Prompt.
- When prompted by the dApp, the Trezor screen will display
Complex typed data. Review raw hash?. - Confirm the primary struct hash matches the transaction to bypass on-device full tree deserialization.
(For similar blind signing and multicall configurations on hardware devices, consult our companion guide on Ledger Blind Signing Auto-Reset EIP-712 Multicall Revert).
5. Security Best Practices When Approving Off-Chain Hashes
- Always Verify VerifyingContract: Before clicking confirm on device hashes, verify that the
verifyingContractaddress on the hardware screen matches the verified contract on Etherscan or Blockscout. - Avoid Obsolete Firmware Versions: Firmware prior to 2.4.0 lacks full EIP-712 schema sanity checks and may silently sign arbitrary hashes.
- Keep Backup Seed Phrases Air-Gapped: Never input your recovery seed into browser forms to “repair” a signing glitch. The 0x6985 error is strictly a transport/serialization error, not a cryptographic seed defect.
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/.