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
|
| const token = this.resolveTssToken(recipient.tokenName ?? txParams.tokenName); | ||
|
|
||
| if (isEmptyTssCalldata(data)) { | ||
| if (token.isToken) { | ||
| await mismatch('token payment carries no ERC-20 transfer calldata'); | ||
| } | ||
| if (!tssValuesEqual(value, recipient.amount)) { | ||
| await mismatch('the transaction amount in txPrebuild does not match the value given by client'); | ||
| } | ||
| if (to !== recipient.address.toLowerCase()) { | ||
| await mismatch('destination address does not match with the recipient address'); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| const transferMethodId = tssMethodId('transfer', TSS_ERC20_TRANSFER_TYPES); | ||
| if (!data.startsWith(transferMethodId)) { | ||
| if (token.confidential) { | ||
| this.warnTssVerificationIncomplete('transfer', `${token.reason}: the payment calldata was not verified`); | ||
| return; | ||
| } | ||
| await mismatch('payment calldata is not a native send or an ERC-20 transfer(address,uint256)'); |
There was a problem hiding this comment.
🔴 Approvers of ERC-7984 confidential sends from a base-coin EVM TSS wallet now get a signing rejection when they approve through the SDK. On the base branch these sign. On that path the stored payment intent carries no token name, so resolveTssToken returns isToken: false. The confidentialTransfer calldata then fails at :3699 with 'payment calldata is not a native send or an ERC-20 transfer(address,uint256)'. Fix: when no token is declared, look up to among this network's ERC-7984 contracts (getErc7984WrappersForNetwork(network).addresses) and only warn on a hit, the same as a declared confidential token.
Why this was flagged
An approver calls pendingApproval.approve(). That runs recreateTxRequest (baseTSSUtils.ts:628-632), which calls signTxRequest with no txParams. resolveEffectiveTxParams (recipientUtils.ts:131-163) then sets type to intent.intentType, which is 'payment' because wallet.ts:4559 stores every 'transfer' that way. The ECDSA callers (ecdsaMPCv2.ts:994/1004, ecdsa.ts:835/842) pass no chainName, so the amount.symbol fallback is skipped. A plain sendMany token recipient gets only amount.symbol (mpcUtils.ts:251), so tokenName ends up undefined. The diff now maps 'payment' to 'transfer' (:3863), and resolveTssToken(undefined) on a base coin returns { isToken: false } (:3629). The confidential check at :3695 is skipped, so the WP-built confidentialTransfer calldata is rejected at :3699. On the base branch only 'transfer' was checked, so this path signed.
Verification: wallet.ts:4559-4562 maps a transfer to intentType: 'payment'. recipientUtils.ts:138-140 takes tokenName only from tokenData?.tokenName. token.confidential is falsy, so line 3699 throws TxIntentMismatchRecipientError. On the base branch the old check was ['transfer'].includes(txParams.type), so these approvals signed.
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