Repository navigation
Conversation
verifyTssTransaction compared only the destination and amount of a single native or ERC-20 transfer declared as `transfer`. It skipped stored `payment` intents, batches, the token contract, native value on token transfers, and the calldata shape, so mismatched bytes could reach signing. Payments: stored `payment` is treated as `transfer`. Explicit calldata must match to/value/data exactly; a native send carries no calldata; an ERC-20 transfer must be canonical, carry no native value and call the declared token contract. Multi-recipient payments must use the network batcher with matching batch/batchTransferFrom arrays, the summed value for native and zero for tokens. Consolidation keeps its base-address checks and adds zero native value, canonical calldata and the declared token contract for ERC-20 sweeps, and no calldata for native sweeps. Sweep amounts are not compared. Unresolved token or batcher metadata and intent types without a verifier log a warning and never suppress other checks. Signing routing and legacy verification are unchanged. Ticket: CECHO-2460 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
There was a problem hiding this comment.
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.
|
@claude review |
|
Claude finished @dpkjnr's task in 29s —— View job Review of #10136: harden EVM TSS payment and consolidation checksI read the Should fix
Worth a look
Notes and nits
|
Stored payment approvals reconstruct recipients without caller txParams. Pass the signing coin's chain name in both ECDSA MPCv2 verification branches so amount.symbol restores tokenName for confidential payments. Cover both signing-byte routes with the real EVM verifier and preserve existing caller-recipient precedence. Ticket: CECHO-2460 Co-authored-by: Ralph CLI <ralph-cli@bitgo.com> Ralph: 18.13.2-bitgo.6 openrouter/deepseek/deepseek-v4.1-flash-1017@default:high+smol:high+task:high openrouter/anthropic/claude-opus-5.5@slow:high+plan:xhigh openrouter/openai/gpt-6.1-terra@vision:low+advisor:medium openrouter/z-ai/glm-5.3-flash@commit:high+memory:low openrouter/inclusionai/ling-3.1-flash@tiny:low openrouter/moonshotai/kimi-k3.1-pro@designer:high openrouter/google/gemini-3-pro-image@image web/exa@web openrouter/microsoft/mai-transcribe-2@dictation openrouter/typesafe/jev-1.14@judge
|
@claude review again |
|
Claude finished @dpkjnr's task in 36s —— View job Re-review of #10136 (second commit
|
There was a problem hiding this comment.
Findings marked 🟡 are optional suggestions and need no follow-up push.
Still open from earlier reviews (1):
- 🔴
modules/abstract-eth/src/abstractEthLikeNewCoins.ts:3699—Approvers of ERC-7984 confidential sends from a base-coin EVM TSS wallet now get a signing rejection when they approve…
If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.
| const token = this.resolveTssToken(recipient.tokenName ?? txParams.tokenName); | ||
|
|
||
| if (isEmptyTssCalldata(data)) { | ||
| if (token.isToken) { | ||
| await mismatch('token payment carries no ERC-20 transfer calldata'); |
There was a problem hiding this comment.
🟡 (optional) Approvers of stored native EVM sends whose intent symbol isn't the exact statics name of the signing chain now get a hard signing rejection. On the base branch these sign. With getChain() now passed at ecdsaMPCv2.ts:994/1004, any symbol other than getChain() becomes tokenName. resolveTssToken then returns isToken: true for a name statics can't resolve (:3636), and the native path throws 'token payment carries no ERC-20 transfer calldata'. Fix: treat a token name statics can't resolve as unverified on every path, so it only warns, as the ERC-20 path already does. The same goes for the native-batch tokens.some(isToken) rejection.
Why this was flagged
This applies only to stored payment intents whose recipient amount.symbol differs from baseCoin.getChain(). Examples are a different case ('ETH'), an alias, or a symbol written by a non-SDK client. From this checkout I could not list which producers do that. mpcUtils.ts:251 only covers SDK-built intents. On approval, resolveEffectiveTxParams (recipientUtils.ts:143-145) now gets chainName from ecdsaMPCv2.ts:994/1004 and sets tokenName = symbol. coins.getOrUndefined (statics map.ts:285) is an exact, case-sensitive lookup, so resolveTssToken returns { isToken: true, reason } at abstractEthLikeNewCoins.ts:3636. verifyTssSinglePayment then rejects a plain native send with empty calldata at :3682. verifyTssBatchPayment likewise rejects a native batch through tokens.some((token) => token.isToken). On the base branch tokenName was never derived on MPCv2, and native transfers checked only value and destination, so these approvals signed. An unresolvable token on the ERC-20 path only warns, so the two paths are inconsistent.
Verification: Trigger: a stored payment intent has a native recipient whose amount.symbol is not baseCoin.getChain() and not a statics name, id or alias of the native coin. ecdsaMPCv2.ts:994/1004 now pass this.baseCoin.getChain(), so recipientUtils.ts sets tokenName = symbol. resolveTssToken then returns { isToken: true } and the native path rejects the signing. On the base branch these signed.
| if (!data.startsWith(transferMethodId)) { | ||
| if (token.confidential) { | ||
| this.warnTssVerificationIncomplete('transfer', `${token.reason}: the payment calldata was not verified`); | ||
| return; |
There was a problem hiding this comment.
🟣 pre-existing, not blocking: Pre-existing, security-relevant: when a payment declares an ERC-7984 token, any contract, calldata and native value still pass with just a warning. The PR describes this as warning-only, but the branch returns before every other check. resolveTssToken (:3638) leaves out the contract it just resolved, and :3695 returns without checking to or value. Fix: return the contract for ERC-7984 tokens too, then require to equal to it, zero native value, and canonical calldata. Also require the selector to be confidentialTransferWithProofMethodId with the recipient decoded by decodeDirectConfidentialTransferCalldata matching, and warn only about the encrypted amount.
A small fix can ride a push you are already making; otherwise a short reply is enough.
Why this was flagged
The trigger is a single-recipient transfer/payment whose recipient.tokenName (or txParams.tokenName, or amount.symbol on MPCv2 stored approvals) names an Erc7984Coin, such as hteth:ctest1. resolveTssToken returns { isToken: true, confidential: true, reason } at :3639 and drops the contract it validated at :3634-3635. In verifyTssSinglePayment, any data that does not start with transfer(address,uint256) hits :3695-3697: it logs a warning and returns. to and value are never compared, so a prebuild with approve(attacker, max) on another token, or any call carrying native value, gets signed. The PR description says "A warning never skips the other checks", but this branch does. The base was just as open for transfer (non-0xa9059cbb data was never checked) and skipped payment entirely, so this is a gap the hardening leaves open, not a regression.
Verification: pre-existing (security-relevant). In verifyTssSinglePayment, when !data.startsWith(transferMethodId) and token.confidential (:3694-3697), the code logs a warning and returns. to, value and the calldata are never compared. So any contract, any calldata and any native value pass for a declared ERC-7984 payment. The base fails the same way by the same route, which makes this pre-existing and not a regression.
Stored pending-approval signing reconstructs recipients from the persisted intent because recreateTxRequest -> signTxRequest carries no caller txParams. For a token payment the only surviving asset hint is amount.symbol, so resolveEffectiveTxParams needs the signing chain name to recover tokenName. The MPCv2 call sites already pass it, but the MPCv1 ECDSA callers did not. With the stricter verifier on this branch, an ERC-7984 stored payment from an MPCv1 wallet was rejected as "not a native send or an ERC-20 transfer", and an ERC-20 payment lost its declared-contract check and warned instead. Pass this.baseCoin.getChain() in both MPCv1 verification branches, matching MPCv2 and EdDSA, and widen the reconstruction JSDoc accordingly. Ticket: CECHO-2460
|
@claude review again |
What
A narrower alternative to #10134 that hardens
verifyTssTransactiononly, assuming the caller passes correcttxParams. Signing routing, sdk-core plumbing, the legacyEvmCoinverifier, FLR and statics are untouched.Payments (
transfer, and storedpaymenttreated the same way)transfer(address,uint256)whose recipient and amount match, with zero native value, sent to the declared token contract (resolved from statics; a token coin declares itself).to,valueanddatamust match exactly. Nonzero native value is allowed.batch(address[],uint256[])arrays matching the intent and value equal to their sum.batchTransferFrom(address,address[],uint256[])with matching arrays, zero value and the declared token contract. Top-leveltokenNameis not required.Consolidation (
verification.consolidationToBaseAddress): the existing base-address checks stay. An ERC-20 sweep must also be canonical, carry zero native value and call the declared token contract; a native sweep must carry no calldata. Sweep amounts are not compared.Warnings only: a token that statics can't resolve, a network with no batcher configured, ERC-20 calldata where no token is declared (recipient, amount and value are still enforced), ERC-7984 payments, and intent types with no verifier (including
contractCall/customTx). A warning never skips the other checks.Deliberately not in this PR
contractCall/customTx: existing upstream tests sendcustomTxthrough this verifier and expect it to pass.signableHexon ECDSA, theEvmCoinlegacy bypass, FLR atomic handling, and the Katana batcher address in statics.Test changes
sdk-coin-eth/test/unit/tssPaymentVerification.ts, which builds real hteth bytes and covers matches and definite mismatches for every rule above. It also checks that a known payment with the wrong function fails, that a missing batcher does not hide wrong batch amounts, and that explicit calldata may carry native value.sdk-coin-xdc/test/unit/xdcToken.ts: the fixture used mainnet USDC's contract fortxdc:tmt, so it now uses the token's contract from statics.sdk-coin-bsc/test/unit/bsc.ts: a tampered WalletConnect payload is now rejected by the exact-calldata check, so the expected message changed.Verification
Ticket: CECHO-2460
🤖 Generated with Claude Code