Skip to content

Commit 2e6ca82

Browse files
committed
fix(bsc): port transfer validation from base class into verifyTssTransaction
Replace the unconditional-return-true stub in Bsc and BscToken with the same 'transfer' calldata validation that AbstractEthLikeNewCoins already performs: - native BNB (data === '0x'): verifies destination address and amount match the declared recipient - BEP-20 transfer() (0xa9059cbb): decodes ABI calldata, verifies recipient address and token amount, including the WalletConnect recipients[0].data fallback The original stub was introduced in commit 3d29436 (COIN-3222, May 2025) to unblock a txHex-decoding crash in the transaction builder. That issue is resolved; this brings BSC to parity with ETH and other AbstractEthLikeNewCoins coins without removing the override. The shallow presence guards (missing txParams, missing params, hop+batch conflict) are retained from the original override, with the addition of the missing txPrebuild.consolidateId check that the base class also performs. Add regression tests confirming: - Native BNB TSS transfer with matching recipient passes - Native BNB TSS transfer with mismatched recipient throws - BEP-20 TSS token transfer with matching calldata recipient passes - BEP-20 TSS token transfer with mismatched calldata recipient throws Ticket: WCI-1169 Session-Id: 4ade7ff7-085e-476d-b280-f4d3dd01c105 Task-Id: 9e3fa483-2a00-47db-aa04-c64b6f003a77
1 parent b968966 commit 2e6ca82

2 files changed

Lines changed: 227 additions & 3 deletions

File tree

modules/sdk-coin-bsc/src/bsc.ts

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,19 @@ import {
55
MPCAlgorithm,
66
MultisigType,
77
multisigTypes,
8+
NO_RECIPIENT_TX_TYPES,
9+
Recipient,
10+
TxIntentMismatchRecipientError,
811
} from '@bitgo/sdk-core';
912
import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics';
10-
import { AbstractEthLikeNewCoins, recoveryBlockchainExplorerQuery } from '@bitgo/abstract-eth';
13+
import {
14+
AbstractEthLikeNewCoins,
15+
getBufferedByteCode,
16+
getRawDecoded,
17+
recoveryBlockchainExplorerQuery,
18+
VerifyEthTransactionOptions,
19+
} from '@bitgo/abstract-eth';
20+
import { addHexPrefix } from 'ethereumjs-util';
1121
import { TransactionBuilder } from './lib';
1222

1323
export class Bsc extends AbstractEthLikeNewCoins {
@@ -58,4 +68,103 @@ export class Bsc extends AbstractEthLikeNewCoins {
5868
return await recoveryBlockchainExplorerQuery(query, explorerUrl as string, apiToken);
5969
}
6070

71+
/**
72+
* Verify if a tss transaction is valid.
73+
*
74+
* Performs the same 'transfer' calldata validation as AbstractEthLikeNewCoins:
75+
* - native BNB (data === '0x'): checks destination address and amount
76+
* - BEP-20 transfer() (0xa9059cbb): decodes calldata, checks destination and
77+
* amount, including the WalletConnect recipients[0].data fallback
78+
*
79+
* @param {VerifyEthTransactionOptions} params
80+
* @returns {Promise<boolean>}
81+
*/
82+
async verifyTssTransaction(params: VerifyEthTransactionOptions): Promise<boolean> {
83+
const { txParams, txPrebuild, wallet } = params;
84+
85+
const throwRecipientMismatch = (message: string, mismatchedRecipients: Recipient[]): never => {
86+
throw new TxIntentMismatchRecipientError(message, undefined, [txParams], txPrebuild?.txHex, mismatchedRecipients);
87+
};
88+
89+
if (
90+
!txParams?.recipients &&
91+
!(
92+
txParams.prebuildTx?.consolidateId ||
93+
txPrebuild?.consolidateId ||
94+
txParams.stakingRequestId ||
95+
txParams.prebuildTx?.stakingRequestId ||
96+
(txParams.type && NO_RECIPIENT_TX_TYPES.has(txParams.type))
97+
)
98+
) {
99+
throw new Error(`missing txParams`);
100+
}
101+
if (!wallet || !txPrebuild) {
102+
throw new Error(`missing params`);
103+
}
104+
if (txParams.hop && txParams.recipients && txParams.recipients.length > 1) {
105+
throw new Error(`tx cannot be both a batch and hop transaction`);
106+
}
107+
108+
if (txParams.type && txParams.type === 'transfer') {
109+
if (txParams.recipients && txParams.recipients.length === 1) {
110+
const recipients = txParams.recipients;
111+
const expectedAmount = recipients[0].amount.toString();
112+
const expectedDestination = recipients[0].address;
113+
114+
const txBuilder = this.getTransactionBuilder();
115+
txBuilder.from(txPrebuild.txHex);
116+
const tx = await txBuilder.build();
117+
const txJson = tx.toJson();
118+
119+
if (txJson.data === '0x') {
120+
if (expectedAmount !== txJson.value) {
121+
throwRecipientMismatch('the transaction amount in txPrebuild does not match the value given by client', [
122+
{ address: txJson.to, amount: txJson.value },
123+
]);
124+
}
125+
if (expectedDestination.toLowerCase() !== txJson.to.toLowerCase()) {
126+
throwRecipientMismatch('destination address does not match with the recipient address', [
127+
{ address: txJson.to, amount: txJson.value },
128+
]);
129+
}
130+
} else if (txJson.data.startsWith('0xa9059cbb')) {
131+
const [recipientAddress, amount] = getRawDecoded(
132+
['address', 'uint256'],
133+
getBufferedByteCode('0xa9059cbb', txJson.data)
134+
);
135+
136+
// Check if recipients[0].data exists (WalletConnect flow)
137+
let expectedRecipientAddress: string;
138+
let expectedTokenAmount: string;
139+
const recipientData = (recipients[0] as any).data;
140+
141+
if (recipientData && recipientData.startsWith('0xa9059cbb')) {
142+
const [expectedRecipient, expectedAmt] = getRawDecoded(
143+
['address', 'uint256'],
144+
getBufferedByteCode('0xa9059cbb', recipientData)
145+
);
146+
expectedRecipientAddress = addHexPrefix(expectedRecipient.toString()).toLowerCase();
147+
expectedTokenAmount = expectedAmt.toString();
148+
} else {
149+
expectedRecipientAddress = expectedDestination.toLowerCase();
150+
expectedTokenAmount = expectedAmount;
151+
}
152+
153+
if (expectedTokenAmount !== amount.toString()) {
154+
throwRecipientMismatch('the transaction amount in txPrebuild does not match the value given by client', [
155+
{ address: addHexPrefix(recipientAddress.toString()), amount: amount.toString() },
156+
]);
157+
}
158+
159+
if (expectedRecipientAddress !== addHexPrefix(recipientAddress.toString()).toLowerCase()) {
160+
throwRecipientMismatch('destination address does not match with the recipient address', [
161+
{ address: addHexPrefix(recipientAddress.toString()), amount: amount.toString() },
162+
]);
163+
}
164+
}
165+
}
166+
}
167+
168+
return true;
169+
}
61170
}

modules/sdk-coin-bsc/src/bscToken.ts

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,23 @@
33
*/
44

55
import { EthLikeTokenConfig, coins } from '@bitgo/statics';
6-
import { BitGoBase, CoinConstructor, NamedCoinConstructor, MPCAlgorithm } from '@bitgo/sdk-core';
7-
import { CoinNames, EthLikeToken } from '@bitgo/abstract-eth';
6+
import {
7+
BitGoBase,
8+
CoinConstructor,
9+
MPCAlgorithm,
10+
NamedCoinConstructor,
11+
NO_RECIPIENT_TX_TYPES,
12+
Recipient,
13+
TxIntentMismatchRecipientError,
14+
} from '@bitgo/sdk-core';
15+
import {
16+
CoinNames,
17+
EthLikeToken,
18+
getBufferedByteCode,
19+
getRawDecoded,
20+
VerifyEthTransactionOptions,
21+
} from '@bitgo/abstract-eth';
22+
import { addHexPrefix } from 'ethereumjs-util';
823
import { TransactionBuilder } from './lib';
924

1025
export { EthLikeTokenConfig };
@@ -43,4 +58,104 @@ export class BscToken extends EthLikeToken {
4358
getFullName(): string {
4459
return 'Bsc Token';
4560
}
61+
62+
/**
63+
* Verify if a tss transaction is valid.
64+
*
65+
* Performs the same 'transfer' calldata validation as AbstractEthLikeNewCoins:
66+
* - native transfer (data === '0x'): checks destination address and amount
67+
* - BEP-20 transfer() (0xa9059cbb): decodes calldata, checks destination and
68+
* amount, including the WalletConnect recipients[0].data fallback
69+
*
70+
* @param {VerifyEthTransactionOptions} params
71+
* @returns {Promise<boolean>}
72+
*/
73+
async verifyTssTransaction(params: VerifyEthTransactionOptions): Promise<boolean> {
74+
const { txParams, txPrebuild, wallet } = params;
75+
76+
const throwRecipientMismatch = (message: string, mismatchedRecipients: Recipient[]): never => {
77+
throw new TxIntentMismatchRecipientError(message, undefined, [txParams], txPrebuild?.txHex, mismatchedRecipients);
78+
};
79+
80+
if (
81+
!txParams?.recipients &&
82+
!(
83+
txParams.prebuildTx?.consolidateId ||
84+
txPrebuild?.consolidateId ||
85+
txParams.stakingRequestId ||
86+
txParams.prebuildTx?.stakingRequestId ||
87+
(txParams.type && NO_RECIPIENT_TX_TYPES.has(txParams.type))
88+
)
89+
) {
90+
throw new Error(`missing txParams`);
91+
}
92+
if (!wallet || !txPrebuild) {
93+
throw new Error(`missing params`);
94+
}
95+
if (txParams.hop && txParams.recipients && txParams.recipients.length > 1) {
96+
throw new Error(`tx cannot be both a batch and hop transaction`);
97+
}
98+
99+
if (txParams.type && txParams.type === 'transfer') {
100+
if (txParams.recipients && txParams.recipients.length === 1) {
101+
const recipients = txParams.recipients;
102+
const expectedAmount = recipients[0].amount.toString();
103+
const expectedDestination = recipients[0].address;
104+
105+
const txBuilder = this.getTransactionBuilder();
106+
txBuilder.from(txPrebuild.txHex);
107+
const tx = await txBuilder.build();
108+
const txJson = tx.toJson();
109+
110+
if (txJson.data === '0x') {
111+
if (expectedAmount !== txJson.value) {
112+
throwRecipientMismatch('the transaction amount in txPrebuild does not match the value given by client', [
113+
{ address: txJson.to, amount: txJson.value },
114+
]);
115+
}
116+
if (expectedDestination.toLowerCase() !== txJson.to.toLowerCase()) {
117+
throwRecipientMismatch('destination address does not match with the recipient address', [
118+
{ address: txJson.to, amount: txJson.value },
119+
]);
120+
}
121+
} else if (txJson.data.startsWith('0xa9059cbb')) {
122+
const [recipientAddress, amount] = getRawDecoded(
123+
['address', 'uint256'],
124+
getBufferedByteCode('0xa9059cbb', txJson.data)
125+
);
126+
127+
// Check if recipients[0].data exists (WalletConnect flow)
128+
let expectedRecipientAddress: string;
129+
let expectedTokenAmount: string;
130+
const recipientData = (recipients[0] as any).data;
131+
132+
if (recipientData && recipientData.startsWith('0xa9059cbb')) {
133+
const [expectedRecipient, expectedAmt] = getRawDecoded(
134+
['address', 'uint256'],
135+
getBufferedByteCode('0xa9059cbb', recipientData)
136+
);
137+
expectedRecipientAddress = addHexPrefix(expectedRecipient.toString()).toLowerCase();
138+
expectedTokenAmount = expectedAmt.toString();
139+
} else {
140+
expectedRecipientAddress = expectedDestination.toLowerCase();
141+
expectedTokenAmount = expectedAmount;
142+
}
143+
144+
if (expectedTokenAmount !== amount.toString()) {
145+
throwRecipientMismatch('the transaction amount in txPrebuild does not match the value given by client', [
146+
{ address: addHexPrefix(recipientAddress.toString()), amount: amount.toString() },
147+
]);
148+
}
149+
150+
if (expectedRecipientAddress !== addHexPrefix(recipientAddress.toString()).toLowerCase()) {
151+
throwRecipientMismatch('destination address does not match with the recipient address', [
152+
{ address: addHexPrefix(recipientAddress.toString()), amount: amount.toString() },
153+
]);
154+
}
155+
}
156+
}
157+
}
158+
159+
return true;
160+
}
46161
}

0 commit comments

Comments
 (0)