Fix: Trezor Error 0x6985 “Action Cancelled” on EIP-712 Typed Signatures
During high-value decentralized finance interactions—such as signing Uniswap Permit2 approvals, executing CowSwap zero-gas orders, or managing multi-signature policies on Safe—users operating hardware wallets like the Trezor Model T, Trezor Safe 3, or Trezor Safe 5 frequently encounter an abrupt signing failure:
TransportError: Trezor error: Failure_ActionCancelled (code 0x6985)
Message: Action cancelled by user
at TrezorConnect.ethereumSignTypedData (trezor-connect.js:412)
at MetaMaskController.signTypedMessage (background.js:1894)
The confusing aspect of this error is that the user never touched the ‘Cancel’ button on the device touchscreen. Instead, the Trezor screen either flashes for a millisecond before resetting to the lock screen, or the host application immediately throws error 0x6985 before the physical device even displays the domain confirmation prompt.
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.