Repository navigation
Description
Part of #44 (ADR 0001). Phase 1b. Extends the shared clear-signing engine and the Intent surface to off-chain signatures — the real drainer vector. Independent of transport; needed by PRD-03 (curated swap needs Permit2/EIP-712) and PRD-04.
Why
A dapp's most dangerous request is an off-chain signature that moves value with no on-chain tx the wallet simulates. In 2024, permit (56.7%) + setOwner (31.9%) = 88.6% of drainer losses ($494M) (research §30). Deckard can sign one structured payload today (SwapOrder/SignOrder, EIP-712) but has no general message-signing path and no decode/clear-signing for arbitrary typed data.
Goals
- Add
personal_sign(EIP-191) +eth_signTypedData_v4(EIP-712) signing, routedpropose → Decision → human approval, never auto-allowed. - Decode + clear-sign before the human: EIP-712 domain binding (chainId + verifyingContract); recognize high-risk shapes (
permit, Permit2, Seaport,setOwner/ownership transfer); a distinct alarming screen for EIP-7702 delegation. - Consume ERC-7730 descriptors where available; explicit "blind signing — can't decode this" fallback for the uncovered long tail.
- Hard-handle chainId-mismatch + unknown
verifyingContract(warn loudly — most wallets silently don't, research §33).
Non-goals
The transport delivering these (PRD-04); building the ERC-7730 registry (we consume); supporting eth_sign (raw 32-byte blind hash) — refuse outright (research §31).
Design
Contract (deckard-contract), mirror the SwapOrder/SignOrder/PendingPayloadView pattern (don't overload Intent, a tx shape):
SignerRequest::ProposeMessage { message: SignMessage }→Decision;SignMessage { request_id }→SignMessageResult { signature }. Never broadcasts.SignMessageenum:PersonalSign { bytes }(decode UTF-8 or hex + caution);TypedDataV4 { typed_data }(parse domain + primaryType).PendingPayloadView::Message(SignMessageView)so the GUI inbox (PendingList) renders it.- New
deny_reasons:ETH_SIGN_REFUSED,CHAINID_MISMATCH,DELEGATION_REFUSED,UNDECODABLE_TYPED_DATA. - Parse untrusted typed-data via the bounded
Reader(strict length/depth caps; no OOM/recursion).
Decision logic (policy.rs): pure evaluate_message(&SignMessage, &Policy, wallet, now) -> Decision next to evaluate/evaluate_order (keep mock⇄daemon parity). Messages always NeedsApproval in v1. EIP-712: deny on CHAINID_MISMATCH; flag unknown verifyingContract; danger-flag (red, top of card) unlimited/large permit & Permit2, Seaport, owner-transfer.
EIP-7702 (research §34): refuse delegation by default (DELEGATION_REFUSED) — its own spec says "there is no safe way" to sign arbitrary delegations. If ever supported: curated trusted-delegator allowlist + a distinct "you are handing control of your account to " screen.
Card (deckard-app): extend the shared clear-signing card to render SignMessageView — plain-language headline, one canonical key/value list, danger red at top, deliberate hold. ERC-7730 descriptor renders human-readable fields; otherwise the explicit blind-sign caution (demoted-but-present). Origin shown unverified.
wallet_addEthereumChain/switchEthereumChain guard (research §36): sign only with user-submitted chainId, never an RPC-returned one; confirm requester + target chain (daemon invariant here; UI lands with PRD-04).
Acceptance tests
eth_sign_refused; typed_data_chainid_mismatch_denies; permit_flagged_danger; delegation_refused; message_never_broadcasts; undecodable_typed_data_bounded (fuzz vector); evaluate_message mock/daemon parity; transcript hygiene (no message bytes/signature in reason strings).
DoD
Shared DoD (#44) plus: ⌘K commands for user-initiated signing; DESIGN.md card spec honored (screenshot in PR); EIP-7702 refusal + blind-sign fallback documented in THREAT-MODEL.md (new message-signing surface).
Sources: research §30–36; EIP-712/-7702/-3085/-3326, ERC-7730; MetaMask MIP-3; scamsniffer 2024; existing swap_order.rs/SignOrder.
Activity
docs(walletbeat): reconcile with ADR 0001 + issue tracking convention
hellno commentedon Jun 14, 2026
Cross-link to #33: PRD-02's "refuse EIP-7702 delegation by default; if ever supported, curated trusted-delegator allowlist" and #33's session-key grant are the same 7702-authorization primitive from two entry points — dapp/message-requested (refuse) vs. Deckard self-originated (allow-if-scoped).
Suggest they share one 7702-auth-signing chokepoint carrying #33's invariants, rather than two paths that can drift:
chain_id = 0refusal,nonce + 1rule.Then PRD-02's
DELEGATION_REFUSEDdefault reads as "refuse unless the delegate is on #33's allowlist and the grant is self-originated" — keeping one allowlist + one set of invariants instead of two implementations to keep in sync.Generated by Claude Code
feat: add message-signing intents