fix(abstract-eth): verify inner batch calldata recipients [CGD-1319]#8772
fix(abstract-eth): verify inner batch calldata recipients [CGD-1319]#8772kamleshmugdiya wants to merge 1 commit into
Conversation
| // matches user intent. Without this, a compromised platform could swap inner recipients while | ||
| // preserving the outer total amount and batcher-address checks. | ||
| if (!txParams.tokenName) { | ||
| await this.verifyBatchInnerRecipients(txPrebuild, recipients, throwRecipientMismatch); |
There was a problem hiding this comment.
shouldn't we do the same for verifyTssTransaction ? method
There was a problem hiding this comment.
Good catch — verifyTssTransaction previously had no batch verification at all (only single-recipient transfer + consolidation paths). I just pushed a commit that adds equivalent inner-recipient verification for the TSS path.
The TSS shape is slightly different from multi-sig:
- Multi-sig: outer
sendMultiSig(batcher, total, batchData, ...)with batch calldata nested as thedatafield - TSS: TSS wallets are EOAs, so the outer tx calls the batcher contract directly —
tx.to = batcherContractAddress,tx.value = total,tx.data = batch(addr[],amt[])(no sendMultiSig wrapper)
I extracted the inner decode/compare into a shared compareBatchCalldataAgainstRecipients helper used by both paths. New TSS-specific tests cover: missing txHex, wrong outer to, wrong outer value, mismatched inner address, and wrong inner selector.
| .should.be.rejectedWith('recipient address of txPrebuild does not match batcher address'); | ||
| }); | ||
|
|
||
| it('should reject a batch txPrebuild that omits txHex', async function () { |
There was a problem hiding this comment.
also add tests for other evm coins since its common method
There was a problem hiding this comment.
Added happy-path + one negative case in sdk-coin-arbeth, sdk-coin-opeth, and sdk-coin-polygon to confirm the abstract verifier picks up each coin's batcher contract address correctly. Each module now has a should verify a batch txPrebuild whose inner calldata matches txParams.recipients test plus a targeted rejection (mismatched address / amount / count) so coverage isn't only on Ethereum.
The verifier itself lives in abstract-eth and is shared by all EVM coins, so the comprehensive test matrix stays in sdk-coin-eth; the per-coin tests are smoke tests for the network-config wiring.
Add the same batch(address[],uint256[]) inner-recipient decoding to verifyTssTransaction, which previously had no batch verification at all. TSS wallets are EOAs and call the batcher contract directly, so the outer transaction has tx.to = batcherContractAddress and tx.data = batch calldata (no sendMultiSig wrapper). The new verifier checks the outer to and value, then decodes and compares each inner pair. Extracts the comparator into compareBatchCalldataAgainstRecipients so the multi-sig and TSS paths share the inner decode/compare logic. Also adds happy-path verifyTransaction batch tests in sdk-coin-arbeth, sdk-coin-opeth, sdk-coin-polygon — the verifier lives in abstract-eth so it applies to all EVM coins, but each per-coin test confirms the batcher contract address is picked up correctly from network config. Addresses review comments on PR #8772: - mullapudipruthvik: shouldn't we do the same for verifyTssTransaction? - mullapudipruthvik: also add tests for other evm coins since its common method Ticket: CGD-1319
8312a05 to
747c372
Compare
Decode the inner batch(address[],uint256[]) calldata embedded in
txPrebuild.txHex and compare each (address, amount) pair to the
user-supplied recipients. Without this check, the verifier validated
only the outer batcher contract address and the total amount, so a
compromised platform could swap inner recipients while preserving the
outer wrapper checks and redirect batched payouts.
Covers both transaction signing paths:
- Multi-sig: outer sendMultiSig(batcher, total, batchData, ...) wraps
the batch calldata as the inner `data` field; verifyTransaction
decodes the wrapper then the inner batch.
- TSS: TSS wallets are EOAs and call the batcher contract directly,
so the outer tx has `to = batcher`, `value = total`, and `data =
batch(addr[],amt[])` with no wrapper. verifyTssTransaction now
decodes the outer tx and compares inner pairs.
Both paths share `compareBatchCalldataAgainstRecipients`, which fails
closed on missing txHex, wrong outer selector / target / value,
unexpected inner selector, or mismatched recipient count / address /
amount.
Tests added in:
- abstract-eth/test/unit/utils.ts: decodeBatchTransferData unit tests
- sdk-coin-eth/test/unit/eth.ts: multi-sig and TSS batch verification
- sdk-coin-arbeth, sdk-coin-opeth, sdk-coin-polygon: per-coin batch
verify smoke tests so each chain's batcher contract address is
exercised through the abstract verifier.
Ticket: CGD-1319
747c372 to
fccd267
Compare
Description
verifyTransactionfor ETH batcher-contract sends only checked (a) the sum oftxParams.recipientsagainsttxPrebuild.recipients[0].amountand (b) that the outer recipient was the network'sbatcherContractAddress. It never decoded the innerbatch(address[],uint256[])calldata embedded intxPrebuild.txHex, andsignTransactionthen loaded thattxHexverbatim.A compromised BitGo platform could preserve both outer checks while swapping the inner
(address, amount)pairs to attacker-controlled addresses, redirecting batched payouts. This PR decodes the inner calldata during verification and compares each pair to user intent.Issue Number
CGD-1319
Type of change
What changed
modules/abstract-eth/src/lib/walletUtil.ts— exportsbatchMethodId(0xc00c4e9e),batchMethodName,batchMethodTypes.modules/abstract-eth/src/lib/iface.ts— addsBatchTransferData/BatchTransferRecipient.modules/abstract-eth/src/lib/utils.ts— addsdecodeBatchTransferData(), modeled after the existingdecode*TransferDatahelpers and reusinggetRawDecoded/getBufferedByteCode.modules/abstract-eth/src/abstractEthLikeNewCoins.ts— adds privateverifyBatchInnerRecipientswhich parsestxPrebuild.txHex, decodes the outersendMultiSig, decodes the innerbatch(address[],uint256[]), and compares each(address, amount)pair totxParams.recipients. Wired into the existing batch path inverifyTransactionfor native batches.The verifier fails closed when:
txPrebuild.txHexis missingsendMultiSigbatch(address[],uint256[])txParams.recipientsToken-batch (
txParams.tokenName) and TSS paths are out of scope per the issue; the codebase has nosendMultisigBatch-style token wrapper, and the existing token branch already expectstxPrebuild.recipients[0].amount === 0.signTransactionis unchanged — it now safely loadstxHexbecause the verifier proves inner calldata matches user intent first.ENS / non-hex
txParamsaddresses skip the address comparison but still enforce the amount check, mirroring the normal-tx path atabstractEthLikeNewCoins.ts:3340.How Has This Been Tested?
modules/abstract-eth/test/unit/utils.ts— 4 tests fordecodeBatchTransferData: hardcoded selector matches runtime-computed value, round-trip encode/decode, wrong-selector rejection, mismatched-array-length rejection.modules/sdk-coin-eth/test/unit/eth.ts— adds abuildBatchTxHexhelper and 5 newverifyTransactiontests: missingtxHex, tampered inner address, tampered inner amount (with outer total preserved), wrong inner selector, mismatched inner recipient count. Updates the existing happy-path batch test to include a validtxHex.Local runs:
yarn unit-testinmodules/abstract-eth— 77 passingyarn unit-testinmodules/sdk-coin-eth— 237 passingyarn lintin both modules — cleanChecklist: