|
| 1 | +/** |
| 2 | + * Invariant #14 — durable-binding source-of-truth verification (issue |
| 3 | + * #460). For any op that binds funds to a durable on-chain object |
| 4 | + * selected from a multi-candidate set (validator pubkey, TRON Super |
| 5 | + * Representative, Compound Comet, Morpho marketId, MarginFi bank, LP |
| 6 | + * tokenId, BTC multisig xpub, allowance spender), the agent MUST |
| 7 | + * source the candidate from an authority outside the MCP, surface it |
| 8 | + * verbatim with provenance, and byte-equality-check the prepared |
| 9 | + * bytes before signing. |
| 10 | + * |
| 11 | + * The MCP-side contribution to that defense: every prepare_* tool in |
| 12 | + * an Inv #14 op class emits a structured `durableBindings: DurableBinding[]` |
| 13 | + * field on its response. The skill consumes it as the assertion target — |
| 14 | + * unambiguous, no parsing of the human-readable `decoded.args` text. |
| 15 | + * |
| 16 | + * Tools intentionally NOT covered: |
| 17 | + * - Plain native-coin sends — recipient is the durable object, but |
| 18 | + * it's already covered by Invariant #1 (recipient cross-check). |
| 19 | + * - Token sends — same; the recipient + the token contract are |
| 20 | + * covered by Inv #1 + Inv #11 today. |
| 21 | + * - Read-only tools — no bytes prepared, no Inv #14 surface. |
| 22 | + */ |
| 23 | + |
| 24 | +/** |
| 25 | + * Closed enum of the durable-object kinds Invariant #14 covers. Add a |
| 26 | + * new kind here only after wiring the corresponding prepare_* tool to |
| 27 | + * emit it; the skill's match logic is keyed on these strings. |
| 28 | + */ |
| 29 | +export type DurableBindingKind = |
| 30 | + | "solana-validator-vote-pubkey" |
| 31 | + | "tron-super-representative-address" |
| 32 | + | "compound-comet-address" |
| 33 | + | "morpho-blue-market-id" |
| 34 | + | "marginfi-bank-pubkey" |
| 35 | + | "uniswap-v3-lp-token-id" |
| 36 | + | "btc-multisig-cosigner-xpub" |
| 37 | + | "approval-spender-address"; |
| 38 | + |
| 39 | +export interface DurableBinding { |
| 40 | + /** Stable kind discriminator the skill matches against. */ |
| 41 | + kind: DurableBindingKind; |
| 42 | + /** |
| 43 | + * Full identifier verbatim, no truncation. Format depends on the |
| 44 | + * kind: base58 for Solana / TRON pubkeys, 0x-prefixed checksum hex |
| 45 | + * for EVM addresses, decimal string for tokenIds, raw xpub string |
| 46 | + * for BTC multisig cosigners, lowercase hex for Morpho marketIds. |
| 47 | + */ |
| 48 | + identifier: string; |
| 49 | + /** |
| 50 | + * Free-form text suggesting where the user should re-verify |
| 51 | + * externally. Per Inv #14, the user MUST source the candidate from |
| 52 | + * an authority outside the MCP's enumeration; this hint nudges them |
| 53 | + * at the right URL / app. Phrased as a recommendation, not a hard |
| 54 | + * statement of trust — the agent renders it verbatim in the |
| 55 | + * verification block. |
| 56 | + */ |
| 57 | + provenanceHint: string; |
| 58 | +} |
| 59 | + |
| 60 | +/** |
| 61 | + * Canonical provenance hints per kind. Centralized so every prepare |
| 62 | + * tool emitting a given kind sends the user to the same external |
| 63 | + * authority — surface drift between tools would erode the user's |
| 64 | + * mental model of "for this kind, I look here". |
| 65 | + */ |
| 66 | +const PROVENANCE_HINTS: Record<DurableBindingKind, string> = { |
| 67 | + "solana-validator-vote-pubkey": |
| 68 | + "Re-verify on stakewiz.com or validators.app — confirm commission, delinquent flag, and that the vote pubkey matches the validator the user actually intends to delegate to.", |
| 69 | + "tron-super-representative-address": |
| 70 | + "Re-verify on tronscan.org/#/sr — confirm SR identity, ranking, and that the base58 address is the validator the user means (brand-name spoof / base58 confusable swap is the b044 attack class).", |
| 71 | + "compound-comet-address": |
| 72 | + "Re-verify on v3.compound.finance/markets — confirm the Comet address matches the (chain, base-asset) the user actually intends to interact with (wrong-Comet routing on the wrong asset is the b053 attack class).", |
| 73 | + "morpho-blue-market-id": |
| 74 | + "Re-verify on app.morpho.org/market/{id} — confirm collateral / loan-token / oracle / IRM / LLTV match the market the user means (b055 attack class: permissionless-market injection with adversarial parameters).", |
| 75 | + "marginfi-bank-pubkey": |
| 76 | + "Re-verify on app.marginfi.com — confirm bank is operational (not paused / killed-by-bankruptcy), oracle setup is healthy, and the asset matches the user's intent (b059 attack class: lookalike-bank injection).", |
| 77 | + "uniswap-v3-lp-token-id": |
| 78 | + "Re-verify on app.uniswap.org/positions/v3/<chain>/<tokenId> — confirm the position owner is your wallet, not an attacker-injected LP NFT enumerated into your portfolio (b063 attack class).", |
| 79 | + "btc-multisig-cosigner-xpub": |
| 80 | + "Re-verify each cosigner xpub against the origin device's backup card / set-up record — never trust an xpub passed to this tool through a third-party communication channel (b098 attack class: attacker xpub embedded as 'co-signer').", |
| 81 | + "approval-spender-address": |
| 82 | + "Re-verify on etherscan.io/address/<spender> — confirm the spender contract identity matches the protocol the user intends to grant allowance to (a086 / b118 attack class: reverse-revoke distraction).", |
| 83 | +}; |
| 84 | + |
| 85 | +/** |
| 86 | + * Build a `DurableBinding` with the canonical provenance hint for the |
| 87 | + * given kind. Prepare tools call this rather than constructing the |
| 88 | + * object literal so all kind ↔ hint pairings live in one place. |
| 89 | + */ |
| 90 | +export function makeDurableBinding( |
| 91 | + kind: DurableBindingKind, |
| 92 | + identifier: string, |
| 93 | +): DurableBinding { |
| 94 | + const provenanceHint = PROVENANCE_HINTS[kind]; |
| 95 | + return { kind, identifier, provenanceHint }; |
| 96 | +} |
0 commit comments