Skip to content

fix(sdk-core): carry TSS intent and strict policy to verifyTransaction - #9972

Open
ralph-bitgo[bot] wants to merge 1 commit into
masterfrom
CECHO-2460-evm-tss-fail-closed-handlers-pt1
Open

ralph-bitgo[bot] wants to merge 1 commit into
masterfrom
CECHO-2460-evm-tss-fail-closed-handlers-pt1

Conversation

@ralph-bitgo

@ralph-bitgo ralph-bitgo Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

What

  • Adds an optional VerifyTransactionOptions.intent (the persisted PopulatedIntent), TssVerificationMismatch / TssVerificationWarning reporting types, and client-side strictTssVerification / onVerificationWarning options on BitGoBase / BitGoAPI (set only from local options, never read from server data).
  • The ecdsa / ecdsaMPCv2 signing paths now pass txRequest.intent and this.baseCoin.getChain() into verifyTransaction / resolveEffectiveTxParams, so the pending-approval path verifies against the intent the user approved (chain arg also fixes token-name resolution from amount.symbol, matching the EdDSA path).
  • Registers the kebab-case DeFi intent spellings (defi-approve, defi-deposit, defi-withdraw, defi-redeem) in NO_RECIPIENT_TX_TYPES. This only waives the missing-recipients guard — the DeFi calldata checks still validate (part 2 of this stack).

Why

  • TOB-BITGOEDMPC-2 / INFOSEC-671: verifyTssTransaction must compare the decoded to / value / data against user intent before the key share signs. On the pending-approval path the intent lives only in txRequest.intent, which the verifier could not see.
  • The kebab-case DeFi throw rejected legitimate DeFi requests (~118 per 90 days) before any verification could run; the fix must not become a silent skip, so the verifier-side DeFi checks land in part 2.

Test plan

  • sdk-core unit tests: 1063 passing on the stack rebased onto current master — NO_RECIPIENT_TX_TYPES contract updated (new DeFi spellings; payment still excluded), kebab DeFi no-recipients guard test, and the MPCv2 signRequestBase regression test now pins that the stored intent reaches verifyTransaction.
  • BitGo SDK CI green on the rebased head.
  • Non-EVM coins unchanged: shared path changes are additive types plus the guarded DeFi spelling additions, covered by the existing recipientUtils / signing suites.

Stack

This PR is part 1 of 4; review and merge in order:

  1. fix(sdk-core): carry TSS intent and strict policy to verifyTransaction #9972 — sdk-core intent + strict policy plumbing (base: master) ← you are here
  2. fix(abstract-eth): fail-closed EVM TSS handlers for verifyTssTransaction #9973 — abstract-eth fail-closed verifier (base: CECHO-2460-evm-tss-fail-closed-handlers-pt1)
  3. test(sdk-coin-evm): cover fail-closed EVM TSS verification paths #9975 — verification test suites (base: CECHO-2460-evm-tss-fail-closed-handlers-pt2)
  4. fix(abstract-eth): verify EVM TSS payments against declared asset #10114 — caller-declared intent and asset binding; fixes the review findings on this stack (base: CECHO-2460-evm-tss-fail-closed-handlers-pt3)

Release #9972 and #9973 together: this PR relaxes the DeFi recipient guard that #9973's handlers rely on.

Ticket: CECHO-2460

@linear-code

linear-code Bot commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

CECHO-2460

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ Compatibility checks are failing on Node.js 27-nightly (experimental, non-blocking). This does not block merge, but it signals an incompatibility to fix before this release becomes supported.

View run

@dpkjnr
dpkjnr added this pull request to stack #9979 October 15, 2026 12:38
dpkjnr
dpkjnr previously requested changes Oct 15, 2026

@dpkjnr dpkjnr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stack review: request changes

Reviewed #9972 → #9973 → #9975, all 20 changed files, with two parallel reviewers. This is a consolidated review on the stack root; the three blocking findings below are in #9973, modules/abstract-eth/src/tssVerification.ts, not standalone defects in the additive API/type plumbing in #9972.

1. [P1] Server calldata overrides a local no-calldata intent — lines 240–245

buildEvmTssIntentContext fills missing local recipient.data from the stored server intent. Both ECDSA signing paths in #9972 now supply that intent alongside local txParams, so the merge also runs on direct sends.

A caller declaring { address: tokenContract, amount: '0' } with no calldata can receive matching outer to/value plus approve(attacker, MAX) in both transaction bytes and stored intent. The merge selects verifyExactCall, which returns zero violations, bypassing the new strict-mode violation check. Without the server-data merge, the same approval produces a violation. This concerns overwriting local intent, not the separate problem of authenticating a stored pending-approval intent.

Requested change: when local recipients exist, preserve their calldata semantics. Absent local data must not authorize server-provided calldata.

2. [P1] Server token metadata overrides the locally selected asset — lines 244–252

The merge copies server tokenContractAddress into a local recipient even when its local tokenName already identifies token A. tokenContractForRecipient then prefers that copied address. Top-level tokenName also prefers the server intent over txParams.

A local USDC transfer accepts identical recipient/amount calldata sent to substituted contract B: zero violations. Without the injected metadata, the token-contract mismatch is flagged. The intended recipient and amount do not establish that the intended asset is being transferred.

Requested change: resolve explicit local token identity first; server metadata must not supersede it.

3. [P1] Native and token payment shapes are interchangeable — lines 368–369, 519–548

Payment handlers choose native/token shapes from decoded calldata rather than binding them to the intended asset. An unresolved token contract skips the contract check.

The following mismatches returned zero violations:

  • Native payment intent → ERC-20 transfer on an arbitrary contract.
  • Token batch intent → native batch() carrying the summed amount.
  • Native batch intent → batchTransferFrom() using an arbitrary token.
  • Batch recipients declaring tokenAddress → transfer of a different token, because that field is ignored here.

Requested change: constrain selectors to the intended asset, resolve its contract consistently, and report unresolved token identity instead of silently skipping validation.

Verification

Executed the added verifier and batch decoders extracted from the PR diffs against real published dependencies in an isolated throwaway smoke harness. The bypass cases above returned zero violations; calldata/token-substitution negative controls returned violations. Honest token-transfer and native-batch controls returned zero violations. Full SDK signing and repository test suites were not run. The new strict-mode gate throws only when violations are present, so these empty results leave it unenforced for the described inputs. Add regression coverage in #9975 for these local-vs-server precedence and wrong-asset cases.

Release ordering

Keep #9972 and #9973 in the same release: #9972 relaxes the DeFi recipient guard before #9973 supplies the canonical handler mapping. Do not publish part 1 independently of part 2.

@dpkjnr
dpkjnr self-requested a review November 3, 2026 06:42
@dpkjnr
dpkjnr force-pushed the CECHO-2460-evm-tss-fail-closed-handlers-pt1 branch from fac3fee to c773059 Compare November 3, 2026 07:01
@dpkjnr
dpkjnr dismissed their stale review November 3, 2026 10:32

Findings 1–3 are fixed in #10114 (part 4 of this stack): caller-declared intent is the only source when the caller declares type/recipients, token identity resolves locally and fails closed when unresolved or conflicting, and payment/batch calldata is bound to the declared asset. Regression tests for each are in #10114.

@dpkjnr
dpkjnr marked this pull request as ready for review November 3, 2026 10:34
@dpkjnr
dpkjnr requested review from a team as code owners November 3, 2026 10:34

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@dpkjnr
dpkjnr requested a review from a team as a code owner November 3, 2026 10:34
What: add an optional VerifyTransactionOptions.intent (the persisted
PopulatedIntent), the TssVerificationWarning reporting types, and the
BitGoBase strictTssVerification / onVerificationWarning client
options. The ecdsa and ecdsaMPCv2 signing paths now pass
txRequest.intent and the chain name into verifyTransaction /
resolveEffectiveTxParams, and the kebab-case DeFi intent spellings
(defi-approve, defi-deposit, defi-withdraw, defi-redeem) join
NO_RECIPIENT_TX_TYPES so stored defi-* intents reach the verifier
instead of failing the missing-recipients guard first.

Why: the pending-approval signing path only passes
resolveEffectiveTxParams output, so fail-closed EVM TSS verification
cannot see the stored intent fields it must compare against, and the
kebab-case DeFi throw rejects legitimate DeFi requests before any
verification runs (TOB-BITGOEDMPC-2 / INFOSEC-671).

Ticket: CECHO-2460
Session-Id: 22db0dc1-cf47-40e2-b717-1bb226a6304c
Task-Id: a8a26387-a332-4be1-aab5-f39deddfc502
@dpkjnr
dpkjnr force-pushed the CECHO-2460-evm-tss-fail-closed-handlers-pt1 branch from c773059 to e95e7a8 Compare November 3, 2026 10:49
@dpkjnr

dpkjnr commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

@claude review

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @dpkjnr's task in 35s —— View job


Review of #9972 (sdk-core intent and strict-policy plumbing)

I read the full diff and checked resolveEffectiveTxParams and IBaseCoin.getChain. I didn't run any tests or build. The change is additive and small, and I found no blocking defects in this PR. A few things are worth fixing or confirming before merge.

Findings

  1. Intent is passed with an unchecked cast (ecdsa.ts ~L840 and L846, ecdsaMPCv2.ts ~L998 and L1008)

    • txRequest.intent as PopulatedIntent is applied even when txRequest.intent is undefined. verifyTransaction receives intent: undefined, which is fine because the field is optional. The cast hides that, though.
    • Nothing at the call site tells a reader that the "no stored intent" case is handled. The verifier must be fail-closed when intent is missing and only txParams are present. Please cover that case in fix(abstract-eth): fail-closed EVM TSS handlers for verifyTssTransaction #9973, or add a short comment here.
  2. Added getChain() call can break coins and tests that stub baseCoin

    • getChain() is on IBaseCoin, and the three mocks in ecdsaMPCv2.ts were updated. Any other test or mock coin that reaches these paths without getChain will now throw a TypeError.
    • Please confirm CI is green for bitgo, abstract-eth and the sdk-coin-* ECDSA signing tests, not just sdk-core. yarn run unit-test --scope bitgo is the one to check.
    • The behaviour change is intended: amount.symbol now resolves to tokenName on the ECDSA paths, matching EdDSA. Only the MPCv2 test pins the intent pass-through. Please add one test asserting that a non-native amount.symbol yields txParams.recipients[].tokenName through signRequestBase, and one for the legacy ecdsa.ts path. The legacy path has no new regression coverage.
  3. defi-redeem and defiRedeem now pass the missing-recipients guard

  4. Strict mode defaults to off, so unknown or unhandled cases only warn

    • strictTssVerification ?? false plus console.warn means that by default mismatches are logged, not blocked. That fits the "opt-in" framing, but it doesn't fully satisfy TOB-BITGOEDMPC-2 for default clients.
    • Please confirm that is the intended rollout. The earlier stack review (the P1 findings in fix(abstract-eth): fail-closed EVM TSS handlers for verifyTssTransaction #9973) also stresses that bypasses return zero violations, which strict mode can't catch.
  5. Minor points

    • BitGoBase.strictTssVerification and onVerificationWarning are readonly and optional on the interface, and BitGoAPI assigns them in the constructor. Any other BitGoBase implementers, such as test doubles, are unaffected.
    • The TssVerificationWarning doc says "Reported through console.warn", but nothing in this PR does that. It lands in fix(abstract-eth): fail-closed EVM TSS handlers for verifyTssTransaction #9973, so the doc describes behaviour that doesn't exist yet in this PR.
    • The doc on VerifyTransactionOptions.intent says "Never server-controlled beyond the tx request itself". The persisted intent is server-supplied data, and the pending-approval path trusts it. This is the root of the earlier review's precedence concerns. I'd reword it to say the intent is untrusted when used as the sole source of truth, and that local txParams must take precedence when present.
    • In the NO_RECIPIENT_TX_TYPES test, defiRedeem and defi-redeem are asserted as members. This contradicts the prior "does not contain" test that this PR removes. That is consistent with the PR's intent, but it again depends on finding 3.

Verdict

The plumbing is sound. I'd ask for: the getChain and tokenName regression tests (finding 2), a decision on redeem (finding 3), and the reworded trust doc (finding 5). Findings 1 and 4 are non-blocking. As noted in the PR, merge and release this together with #9973. The three P1 verifier issues from the earlier review are in #9973, not in this PR.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment on lines 37 to +47
'defiApprove',
'defi-approve',
'defiDeposit',
'defi-deposit',
'defiWithdraw',
'defi-withdraw',
// defi-redeem has no EVM verifier handler and is flagged as a violation there;
// listed here so the pending-approval path reaches the verifier instead of
// failing earlier on the missing-recipients guard.
'defi-redeem',
'defiRedeem',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Security: with only this PR merged, stored DeFi intents with no txParams get signed with no calldata check at all, where the base branch rejected them. The new defi-approve/defi-deposit/defi-withdraw/defi-redeem/defiRedeem entries pass both no-recipient guards. The EVM DeFi checks in abstractEthLikeNewCoins.ts:3659 only match camelCase defiApprove/defiDeposit/defiWithdraw. Fix: keep these spellings out of NO_RECIPIENT_TX_TYPES until the verifier rejects or checks every one of them, or ship that verifier change in this PR. The comments at recipientUtils.ts:35 and :43 describe checks that do not exist on master. [also at: modules/sdk-coin-evm/src/evmCoin.ts:113 - Security: TSS wallets on non-EIP-1559 EVM chains would sign pending-approval DeFi transactions without any calldata check.]

Why this was flagged

The trigger is the pending-approval path: pendingApproval.approve() -> recreateTxRequest() -> signTxRequest() with txParams undefined, on a stored intent whose intentType is 'defi-deposit' (the spelling wallet.ts:5004 and defiVault.ts:144 write). resolveEffectiveTxParams (recipientUtils.ts:170-175) sets txParams.type = 'defi-deposit'. The guard at recipientUtils.ts:216 no longer throws. In verifyTssTransaction, the guard at abstractEthLikeNewCoins.ts:3583 (and evmCoin.ts:113) passes because NO_RECIPIENT_TX_TYPES now has the kebab spelling. The DeFi block at abstractEthLikeNewCoins.ts:3659 only checks ['defiApprove','defiDeposit','defiWithdraw'], so it is skipped, and the key share signs whatever to/value/data the server built. 'defi-redeem' and 'defiRedeem' have no handler at all, so the comment's claim that they are "flagged as a violation there" is false on master. On the base branch, resolveEffectiveTxParams threw InvalidTransactionError before signing. The PR says the verifier lands in a separate part 2 (#9973), but nothing in this change enforces that the two ship together.

Verification: On the base, the guard at recipientUtils.ts:214-222 threw "Recipient details are required..." because the kebab spellings were not in the set. This diff adds them, so it no longer throws. The DeFi calldata block at abstractEthLikeNewCoins.ts:3659 only matches ['defiApprove', 'defiDeposit', 'defiWithdraw']. With this PR they are signed with no check of the server-built calldata.

Comment on lines +835 to +837
assert.strictEqual(
verifyCallArgs.intent,
(txRequest as any).intent,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Nit (optional): the new assertion adds an avoidable any cast, (txRequest as any).intent, which goes against the CLAUDE.md rule to avoid any. Fix: use txRequest.intent, since txRequest is already typed as TxRequest and has an intent field.

Why this was flagged

There is no runtime impact. At ecdsaMPCv2.ts:811, txRequest is declared as unknown as TxRequest, and TxRequest declares intent. That makes the as any in the new assertion unnecessary, and it goes against the root CLAUDE.md TypeScript rule to avoid the any type.

Verification: Severity: nit. There is no runtime impact. The diff adds (txRequest as any).intent in the new assertion at modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts:837. TxRequest in modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts:621 declares intent: unknown; at line 630. Since assert.strictEqual takes unknown, writing txRequest.intent type-checks without the cast. It goes against the root CLAUDE.md line 88.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants