Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 79 additions & 5 deletions modules/sdk-coin-bsc/src/bsc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,21 @@ import {
MultisigType,
multisigTypes,
NO_RECIPIENT_TX_TYPES,
Recipient,
TxIntentMismatchRecipientError,
} from '@bitgo/sdk-core';
import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics';
import {
AbstractEthLikeNewCoins,
getBufferedByteCode,
getRawDecoded,
recoveryBlockchainExplorerQuery,
VerifyEthTransactionOptions,
} from '@bitgo/abstract-eth';
import { TransactionBuilder } from './lib';

const addHexPrefix = (hex: string): string => (hex.startsWith('0x') ? hex : `0x${hex}`);

export class Bsc extends AbstractEthLikeNewCoins {
protected constructor(bitgo: BitGoBase, staticsCoin?: Readonly<StaticsBaseCoin>) {
super(bitgo, staticsCoin);
Expand Down Expand Up @@ -64,20 +70,28 @@ export class Bsc extends AbstractEthLikeNewCoins {
}

/**
* Verify if a tss transaction is valid
* Verify if a tss transaction is valid.
*
* Performs the same 'transfer' calldata validation as AbstractEthLikeNewCoins:
* - native BNB (data === '0x'): checks destination address and amount
* - BEP-20 transfer() (0xa9059cbb): decodes calldata, checks destination and
* amount, including the WalletConnect recipients[0].data fallback
*
* @param {VerifyEthTransactionOptions} params
* @param {TransactionParams} params.txParams - params object passed to send
* @param {TransactionPrebuild} params.txPrebuild - prebuild object returned by server
* @param {Wallet} params.wallet - Wallet object to obtain keys to verify against
* @returns {boolean}
* @returns {Promise<boolean>}
*/
async verifyTssTransaction(params: VerifyEthTransactionOptions): Promise<boolean> {
const { txParams, txPrebuild, wallet } = params;

const throwRecipientMismatch = (message: string, mismatchedRecipients: Recipient[]): never => {
throw new TxIntentMismatchRecipientError(message, undefined, [txParams], txPrebuild?.txHex, mismatchedRecipients);
};

if (
!txParams?.recipients &&
!(
txParams.prebuildTx?.consolidateId ||
txPrebuild?.consolidateId ||
txParams.stakingRequestId ||
txParams.prebuildTx?.stakingRequestId ||
(txParams.type && NO_RECIPIENT_TX_TYPES.has(txParams.type))
Expand All @@ -92,6 +106,66 @@ export class Bsc extends AbstractEthLikeNewCoins {
throw new Error(`tx cannot be both a batch and hop transaction`);
}

if (txParams.type && txParams.type === 'transfer') {
if (txParams.recipients && txParams.recipients.length === 1) {
const recipients = txParams.recipients;
const expectedAmount = recipients[0].amount.toString();
const expectedDestination = recipients[0].address;

const txBuilder = this.getTransactionBuilder();
txBuilder.from(txPrebuild.txHex);
const tx = await txBuilder.build();
const txJson = tx.toJson();

if (txJson.data === '0x') {
if (expectedAmount !== txJson.value) {
throwRecipientMismatch('the transaction amount in txPrebuild does not match the value given by client', [
{ address: txJson.to, amount: txJson.value },
]);
}
if (expectedDestination.toLowerCase() !== txJson.to.toLowerCase()) {
throwRecipientMismatch('destination address does not match with the recipient address', [
{ address: txJson.to, amount: txJson.value },
]);
}
} else if (txJson.data.startsWith('0xa9059cbb')) {
const [recipientAddress, amount] = getRawDecoded(
['address', 'uint256'],
getBufferedByteCode('0xa9059cbb', txJson.data)
);

// Check if recipients[0].data exists (WalletConnect flow)
let expectedRecipientAddress: string;
let expectedTokenAmount: string;
const recipientData = (recipients[0] as any).data;

if (recipientData && recipientData.startsWith('0xa9059cbb')) {
const [expectedRecipient, expectedAmt] = getRawDecoded(
['address', 'uint256'],
getBufferedByteCode('0xa9059cbb', recipientData)
);
expectedRecipientAddress = addHexPrefix(expectedRecipient.toString()).toLowerCase();
expectedTokenAmount = expectedAmt.toString();
} else {
expectedRecipientAddress = expectedDestination.toLowerCase();
expectedTokenAmount = expectedAmount;
}

if (expectedTokenAmount !== amount.toString()) {
throwRecipientMismatch('the transaction amount in txPrebuild does not match the value given by client', [
{ address: addHexPrefix(recipientAddress.toString()), amount: amount.toString() },
]);
}

if (expectedRecipientAddress !== addHexPrefix(recipientAddress.toString()).toLowerCase()) {
throwRecipientMismatch('destination address does not match with the recipient address', [
{ address: addHexPrefix(recipientAddress.toString()), amount: amount.toString() },
]);
}
}
}
}

return true;
}
}
99 changes: 92 additions & 7 deletions modules/sdk-coin-bsc/src/bscToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,26 @@
*/

import { EthLikeTokenConfig, coins } from '@bitgo/statics';
import { BitGoBase, CoinConstructor, NamedCoinConstructor, MPCAlgorithm, NO_RECIPIENT_TX_TYPES } from '@bitgo/sdk-core';
import { CoinNames, EthLikeToken, VerifyEthTransactionOptions } from '@bitgo/abstract-eth';
import {
BitGoBase,
CoinConstructor,
MPCAlgorithm,
NamedCoinConstructor,
NO_RECIPIENT_TX_TYPES,
Recipient,
TxIntentMismatchRecipientError,
} from '@bitgo/sdk-core';
import {
CoinNames,
EthLikeToken,
getBufferedByteCode,
getRawDecoded,
VerifyEthTransactionOptions,
} from '@bitgo/abstract-eth';
import { TransactionBuilder } from './lib';

const addHexPrefix = (hex: string): string => (hex.startsWith('0x') ? hex : `0x${hex}`);

export { EthLikeTokenConfig };

export class BscToken extends EthLikeToken {
Expand Down Expand Up @@ -43,21 +59,30 @@ export class BscToken extends EthLikeToken {
getFullName(): string {
return 'Bsc Token';
}

/**
* Verify if a tss transaction is valid
* Verify if a tss transaction is valid.
*
* Performs the same 'transfer' calldata validation as AbstractEthLikeNewCoins:
* - native transfer (data === '0x'): checks destination address and amount
* - BEP-20 transfer() (0xa9059cbb): decodes calldata, checks destination and
* amount, including the WalletConnect recipients[0].data fallback
*
* @param {VerifyEthTransactionOptions} params
* @param {TransactionParams} params.txParams - params object passed to send
* @param {TransactionPrebuild} params.txPrebuild - prebuild object returned by server
* @param {Wallet} params.wallet - Wallet object to obtain keys to verify against
* @returns {boolean}
* @returns {Promise<boolean>}
*/
async verifyTssTransaction(params: VerifyEthTransactionOptions): Promise<boolean> {
const { txParams, txPrebuild, wallet } = params;

const throwRecipientMismatch = (message: string, mismatchedRecipients: Recipient[]): never => {
throw new TxIntentMismatchRecipientError(message, undefined, [txParams], txPrebuild?.txHex, mismatchedRecipients);
};

if (
!txParams?.recipients &&
!(
txParams.prebuildTx?.consolidateId ||
txPrebuild?.consolidateId ||
txParams.stakingRequestId ||
txParams.prebuildTx?.stakingRequestId ||
(txParams.type && NO_RECIPIENT_TX_TYPES.has(txParams.type))
Expand All @@ -72,6 +97,66 @@ export class BscToken extends EthLikeToken {
throw new Error(`tx cannot be both a batch and hop transaction`);
}

if (txParams.type && txParams.type === 'transfer') {
if (txParams.recipients && txParams.recipients.length === 1) {
const recipients = txParams.recipients;
const expectedAmount = recipients[0].amount.toString();
const expectedDestination = recipients[0].address;

const txBuilder = this.getTransactionBuilder();
txBuilder.from(txPrebuild.txHex);
const tx = await txBuilder.build();
const txJson = tx.toJson();

if (txJson.data === '0x') {
if (expectedAmount !== txJson.value) {
throwRecipientMismatch('the transaction amount in txPrebuild does not match the value given by client', [
{ address: txJson.to, amount: txJson.value },
]);
}
if (expectedDestination.toLowerCase() !== txJson.to.toLowerCase()) {
throwRecipientMismatch('destination address does not match with the recipient address', [
{ address: txJson.to, amount: txJson.value },
]);
}
} else if (txJson.data.startsWith('0xa9059cbb')) {
const [recipientAddress, amount] = getRawDecoded(
['address', 'uint256'],
getBufferedByteCode('0xa9059cbb', txJson.data)
);

// Check if recipients[0].data exists (WalletConnect flow)
let expectedRecipientAddress: string;
let expectedTokenAmount: string;
const recipientData = (recipients[0] as any).data;

if (recipientData && recipientData.startsWith('0xa9059cbb')) {
const [expectedRecipient, expectedAmt] = getRawDecoded(
['address', 'uint256'],
getBufferedByteCode('0xa9059cbb', recipientData)
);
expectedRecipientAddress = addHexPrefix(expectedRecipient.toString()).toLowerCase();
expectedTokenAmount = expectedAmt.toString();
} else {
expectedRecipientAddress = expectedDestination.toLowerCase();
expectedTokenAmount = expectedAmount;
}

if (expectedTokenAmount !== amount.toString()) {
throwRecipientMismatch('the transaction amount in txPrebuild does not match the value given by client', [
{ address: addHexPrefix(recipientAddress.toString()), amount: amount.toString() },
]);
}

if (expectedRecipientAddress !== addHexPrefix(recipientAddress.toString()).toLowerCase()) {
throwRecipientMismatch('destination address does not match with the recipient address', [
{ address: addHexPrefix(recipientAddress.toString()), amount: amount.toString() },
]);
}
}
}
}

return true;
}
}
Loading
Loading