@@ -23,6 +23,7 @@ import {
2323 MPCTx ,
2424 MPCTxs ,
2525 ParsedTransaction ,
26+ ITransactionRecipient ,
2627 ParseTransactionOptions ,
2728 PrebuildTransactionResult ,
2829 PresignTransactionOptions as BasePresignTransactionOptions ,
@@ -71,8 +72,11 @@ import secp256k1 from 'secp256k1';
7172import { AbstractEthLikeCoin } from './abstractEthLikeCoin' ;
7273import { EthLikeToken } from './ethLikeToken' ;
7374import {
75+ batchMethodId ,
7476 calculateForwarderV1Address ,
7577 coinFamiliesWithL1Fees ,
78+ decodeBatchTransferData ,
79+ decodeNativeTransferData ,
7680 decodeTransferData ,
7781 ERC1155TransferBuilder ,
7882 ERC721TransferBuilder ,
@@ -83,6 +87,7 @@ import {
8387 getRawDecoded ,
8488 getToken ,
8589 KeyPair as KeyPairLib ,
90+ sendMultisigMethodId ,
8691 TransactionBuilder ,
8792 TransferBuilder ,
8893} from './lib' ;
@@ -1633,6 +1638,87 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
16331638 } ;
16341639 }
16351640
1641+ /**
1642+ * Verify that the inner batch(address[],uint256[]) calldata embedded in txPrebuild.txHex matches
1643+ * the user-supplied recipients. Throws via throwRecipientMismatch if any pair differs or if the
1644+ * calldata cannot be decoded. The verifier fails closed: missing txHex, an unexpected outer
1645+ * selector, or an unexpected inner selector all reject.
1646+ */
1647+ private async verifyBatchInnerRecipients (
1648+ txPrebuild : TransactionPrebuild ,
1649+ recipients : ITransactionRecipient [ ] ,
1650+ throwRecipientMismatch : ( message : string , mismatchedRecipients : Recipient [ ] ) => Promise < never >
1651+ ) : Promise < void > {
1652+ if ( ! txPrebuild . txHex ) {
1653+ await throwRecipientMismatch ( 'batch txPrebuild missing txHex required for inner calldata verification' , [ ] ) ;
1654+ return ;
1655+ }
1656+
1657+ let outerCalldata : string ;
1658+ try {
1659+ const txBuffer = optionalDeps . ethUtil . toBuffer ( txPrebuild . txHex ) ;
1660+ const decodedTx = optionalDeps . EthTx . TransactionFactory . fromSerializedData ( txBuffer ) ;
1661+ outerCalldata = optionalDeps . ethUtil . bufferToHex ( decodedTx . data ) ;
1662+ } catch ( e ) {
1663+ await throwRecipientMismatch ( `failed to parse batch txHex: ${ e instanceof Error ? e . message : String ( e ) } ` , [ ] ) ;
1664+ return ;
1665+ }
1666+
1667+ if ( ! outerCalldata . toLowerCase ( ) . startsWith ( sendMultisigMethodId ) ) {
1668+ await throwRecipientMismatch ( 'batch txPrebuild outer call is not sendMultiSig' , [ ] ) ;
1669+ return ;
1670+ }
1671+
1672+ let innerBatchData : string ;
1673+ try {
1674+ innerBatchData = decodeNativeTransferData ( outerCalldata ) . data ;
1675+ } catch ( e ) {
1676+ await throwRecipientMismatch (
1677+ `failed to decode outer sendMultiSig wrapper: ${ e instanceof Error ? e . message : String ( e ) } ` ,
1678+ [ ]
1679+ ) ;
1680+ return ;
1681+ }
1682+
1683+ if ( ! innerBatchData || ! innerBatchData . toLowerCase ( ) . startsWith ( batchMethodId ) ) {
1684+ await throwRecipientMismatch ( 'batch txPrebuild inner method selector is not batch(address[],uint256[])' , [ ] ) ;
1685+ return ;
1686+ }
1687+
1688+ let decoded ;
1689+ try {
1690+ decoded = decodeBatchTransferData ( innerBatchData ) ;
1691+ } catch ( e ) {
1692+ await throwRecipientMismatch (
1693+ `failed to decode inner batch calldata: ${ e instanceof Error ? e . message : String ( e ) } ` ,
1694+ [ ]
1695+ ) ;
1696+ return ;
1697+ }
1698+
1699+ if ( decoded . recipients . length !== recipients . length ) {
1700+ await throwRecipientMismatch (
1701+ `batch txPrebuild inner recipient count (${ decoded . recipients . length } ) does not match txParams (${ recipients . length } )` ,
1702+ decoded . recipients
1703+ ) ;
1704+ return ;
1705+ }
1706+
1707+ for ( let i = 0 ; i < recipients . length ; i ++ ) {
1708+ const expected = recipients [ i ] ;
1709+ const actual = decoded . recipients [ i ] ;
1710+ // Skip address comparison for non-hex inputs (e.g. unresolved ENS); mirrors normal-tx path.
1711+ if ( this . isETHAddress ( expected . address ) && expected . address . toLowerCase ( ) !== actual . address . toLowerCase ( ) ) {
1712+ await throwRecipientMismatch ( 'batch txPrebuild inner recipient address does not match txParams' , [ actual ] ) ;
1713+ return ;
1714+ }
1715+ if ( ! new BigNumber ( expected . amount ) . isEqualTo ( actual . amount ) ) {
1716+ await throwRecipientMismatch ( 'batch txPrebuild inner recipient amount does not match txParams' , [ actual ] ) ;
1717+ return ;
1718+ }
1719+ }
1720+ }
1721+
16361722 /**
16371723 * Extract recipients from transaction hex
16381724 * @param txHex - The transaction hex string
@@ -3325,6 +3411,13 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
33253411 { address : txPrebuild . recipients [ 0 ] . address , amount : txPrebuild . recipients [ 0 ] . amount . toString ( ) } ,
33263412 ] ) ;
33273413 }
3414+
3415+ // Decode the inner batch(address[],uint256[]) calldata and verify each (address, amount) pair
3416+ // matches user intent. Without this, a compromised platform could swap inner recipients while
3417+ // preserving the outer total amount and batcher-address checks.
3418+ if ( ! txParams . tokenName ) {
3419+ await this . verifyBatchInnerRecipients ( txPrebuild , recipients , throwRecipientMismatch ) ;
3420+ }
33283421 } else {
33293422 // Check recipient address and amount for normal transaction
33303423 if ( recipients . length !== 1 ) {
0 commit comments