Skip to content

Commit 4714aa6

Browse files
committed
feat: replace generic Error with SuspiciousTransactionError
Ticket: WP-6189
1 parent 8361510 commit 4714aa6

File tree

4 files changed

+53
-32
lines changed

4 files changed

+53
-32
lines changed

modules/abstract-eth/src/abstractEthLikeNewCoins.ts

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
PresignTransactionOptions as BasePresignTransactionOptions,
2929
Recipient,
3030
SignTransactionOptions as BaseSignTransactionOptions,
31+
SuspiciousTransactionError,
3132
TransactionParams,
3233
TransactionPrebuild as BaseTransactionPrebuild,
3334
TransactionRecipient,
@@ -2777,13 +2778,13 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
27772778
(txParams.type && ['acceleration', 'fillNonce', 'transferToken', 'tokenApproval'].includes(txParams.type))
27782779
)
27792780
) {
2780-
throw new Error(`missing txParams`);
2781+
throw new SuspiciousTransactionError(`missing txParams`);
27812782
}
27822783
if (!wallet || !txPrebuild) {
2783-
throw new Error(`missing params`);
2784+
throw new SuspiciousTransactionError(`missing params`);
27842785
}
27852786
if (txParams.hop && txParams.recipients && txParams.recipients.length > 1) {
2786-
throw new Error(`tx cannot be both a batch and hop transaction`);
2787+
throw new SuspiciousTransactionError(`tx cannot be both a batch and hop transaction`);
27872788
}
27882789

27892790
if (txParams.type && ['transfer'].includes(txParams.type)) {
@@ -2798,21 +2799,25 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
27982799
const txJson = tx.toJson();
27992800
if (txJson.data === '0x') {
28002801
if (expectedAmount !== txJson.value) {
2801-
throw new Error('the transaction amount in txPrebuild does not match the value given by client');
2802+
throw new SuspiciousTransactionError(
2803+
'the transaction amount in txPrebuild does not match the value given by client'
2804+
);
28022805
}
28032806
if (expectedDestination.toLowerCase() !== txJson.to.toLowerCase()) {
2804-
throw new Error('destination address does not match with the recipient address');
2807+
throw new SuspiciousTransactionError('destination address does not match with the recipient address');
28052808
}
28062809
} else if (txJson.data.startsWith('0xa9059cbb')) {
28072810
const [recipientAddress, amount] = getRawDecoded(
28082811
['address', 'uint256'],
28092812
getBufferedByteCode('0xa9059cbb', txJson.data)
28102813
);
28112814
if (expectedAmount !== amount.toString()) {
2812-
throw new Error('the transaction amount in txPrebuild does not match the value given by client');
2815+
throw new SuspiciousTransactionError(
2816+
'the transaction amount in txPrebuild does not match the value given by client'
2817+
);
28132818
}
28142819
if (expectedDestination.toLowerCase() !== addHexPrefix(recipientAddress.toString()).toLowerCase()) {
2815-
throw new Error('destination address does not match with the recipient address');
2820+
throw new SuspiciousTransactionError('destination address does not match with the recipient address');
28162821
}
28172822
}
28182823
}
@@ -2839,20 +2844,22 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
28392844
}
28402845

28412846
if (!txParams?.recipients || !txPrebuild?.recipients || !wallet) {
2842-
throw new Error(`missing params`);
2847+
throw new SuspiciousTransactionError(`missing params`);
28432848
}
28442849
if (txParams.hop && txParams.recipients.length > 1) {
2845-
throw new Error(`tx cannot be both a batch and hop transaction`);
2850+
throw new SuspiciousTransactionError(`tx cannot be both a batch and hop transaction`);
28462851
}
28472852
if (txPrebuild.recipients.length > 1) {
2848-
throw new Error(
2853+
throw new SuspiciousTransactionError(
28492854
`${this.getChain()} doesn't support sending to more than 1 destination address within a single transaction. Try again, using only a single recipient.`
28502855
);
28512856
}
28522857
if (txParams.hop && txPrebuild.hopTransaction) {
28532858
// Check recipient amount for hop transaction
28542859
if (txParams.recipients.length !== 1) {
2855-
throw new Error(`hop transaction only supports 1 recipient but ${txParams.recipients.length} found`);
2860+
throw new SuspiciousTransactionError(
2861+
`hop transaction only supports 1 recipient but ${txParams.recipients.length} found`
2862+
);
28562863
}
28572864

28582865
// Check tx sends to hop address
@@ -2862,7 +2869,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
28622869
const expectedHopAddress = optionalDeps.ethUtil.stripHexPrefix(decodedHopTx.getSenderAddress().toString());
28632870
const actualHopAddress = optionalDeps.ethUtil.stripHexPrefix(txPrebuild.recipients[0].address);
28642871
if (expectedHopAddress.toLowerCase() !== actualHopAddress.toLowerCase()) {
2865-
throw new Error('recipient address of txPrebuild does not match hop address');
2872+
throw new SuspiciousTransactionError('recipient address of txPrebuild does not match hop address');
28662873
}
28672874

28682875
// Convert TransactionRecipient array to Recipient array
@@ -2880,15 +2887,17 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
28802887
if (txParams.tokenName) {
28812888
const expectedTotalAmount = new BigNumber(0);
28822889
if (!expectedTotalAmount.isEqualTo(txPrebuild.recipients[0].amount)) {
2883-
throw new Error('batch token transaction amount in txPrebuild should be zero for token transfers');
2890+
throw new SuspiciousTransactionError(
2891+
'batch token transaction amount in txPrebuild should be zero for token transfers'
2892+
);
28842893
}
28852894
} else {
28862895
let expectedTotalAmount = new BigNumber(0);
28872896
for (let i = 0; i < txParams.recipients.length; i++) {
28882897
expectedTotalAmount = expectedTotalAmount.plus(txParams.recipients[i].amount);
28892898
}
28902899
if (!expectedTotalAmount.isEqualTo(txPrebuild.recipients[0].amount)) {
2891-
throw new Error(
2900+
throw new SuspiciousTransactionError(
28922901
'batch transaction amount in txPrebuild received from BitGo servers does not match txParams supplied by client'
28932902
);
28942903
}
@@ -2900,29 +2909,33 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
29002909
!batcherContractAddress ||
29012910
batcherContractAddress.toLowerCase() !== txPrebuild.recipients[0].address.toLowerCase()
29022911
) {
2903-
throw new Error('recipient address of txPrebuild does not match batcher address');
2912+
throw new SuspiciousTransactionError('recipient address of txPrebuild does not match batcher address');
29042913
}
29052914
} else {
29062915
// Check recipient address and amount for normal transaction
29072916
if (txParams.recipients.length !== 1) {
2908-
throw new Error(`normal transaction only supports 1 recipient but ${txParams.recipients.length} found`);
2917+
throw new SuspiciousTransactionError(
2918+
`normal transaction only supports 1 recipient but ${txParams.recipients.length} found`
2919+
);
29092920
}
29102921
const expectedAmount = new BigNumber(txParams.recipients[0].amount);
29112922
if (!expectedAmount.isEqualTo(txPrebuild.recipients[0].amount)) {
2912-
throw new Error(
2923+
throw new SuspiciousTransactionError(
29132924
'normal transaction amount in txPrebuild received from BitGo servers does not match txParams supplied by client'
29142925
);
29152926
}
29162927
if (
29172928
this.isETHAddress(txParams.recipients[0].address) &&
29182929
txParams.recipients[0].address !== txPrebuild.recipients[0].address
29192930
) {
2920-
throw new Error('destination address in normal txPrebuild does not match that in txParams supplied by client');
2931+
throw new SuspiciousTransactionError(
2932+
'destination address in normal txPrebuild does not match that in txParams supplied by client'
2933+
);
29212934
}
29222935
}
29232936
// Check coin is correct for all transaction types
29242937
if (!this.verifyCoin(txPrebuild)) {
2925-
throw new Error(`coin in txPrebuild did not match that in txParams supplied by client`);
2938+
throw new SuspiciousTransactionError(`coin in txPrebuild did not match that in txParams supplied by client`);
29262939
}
29272940
return true;
29282941
}

modules/abstract-utxo/src/transaction/descriptor/verifyTransaction.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as utxolib from '@bitgo/utxo-lib';
2-
import { ITransactionRecipient, VerifyTransactionOptions } from '@bitgo/sdk-core';
2+
import { ITransactionRecipient, SuspiciousTransactionError, VerifyTransactionOptions } from '@bitgo/sdk-core';
33
import { DescriptorMap } from '@bitgo/utxo-core/descriptor';
44

55
import { AbstractUtxoCoin, BaseOutput, BaseParsedTransactionOutputs } from '../../abstractUtxoCoin';
@@ -74,7 +74,7 @@ export async function verifyTransaction(
7474
): Promise<boolean> {
7575
const tx = coin.decodeTransactionFromPrebuild(params.txPrebuild);
7676
if (!(tx instanceof utxolib.bitgo.UtxoPsbt)) {
77-
throw new Error('unexpected transaction type');
77+
throw new SuspiciousTransactionError('unexpected transaction type');
7878
}
7979
assertValidTransaction(tx, descriptorMap, params.txParams.recipients ?? [], tx.network);
8080
return true;

modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import buildDebug from 'debug';
22
import _ from 'lodash';
33
import BigNumber from 'bignumber.js';
4-
import { BitGoBase } from '@bitgo/sdk-core';
4+
import { BitGoBase, SuspiciousTransactionError } from '@bitgo/sdk-core';
55
import * as utxolib from '@bitgo/utxo-lib';
66

77
import { AbstractUtxoCoin, Output, ParsedTransaction, VerifyTransactionOptions } from '../../abstractUtxoCoin';
@@ -33,11 +33,11 @@ export async function verifyTransaction<TNumber extends bigint | number>(
3333
const { txParams, txPrebuild, wallet, verification = {}, reqId } = params;
3434

3535
if (!_.isUndefined(verification.disableNetworking) && !_.isBoolean(verification.disableNetworking)) {
36-
throw new Error('verification.disableNetworking must be a boolean');
36+
throw new SuspiciousTransactionError('verification.disableNetworking must be a boolean');
3737
}
3838
const isPsbt = txPrebuild.txHex && utxolib.bitgo.isPsbt(txPrebuild.txHex);
3939
if (isPsbt && txPrebuild.txInfo?.unspents) {
40-
throw new Error('should not have unspents in txInfo for psbt');
40+
throw new SuspiciousTransactionError('should not have unspents in txInfo for psbt');
4141
}
4242
const disableNetworking = !!verification.disableNetworking;
4343
const parsedTransaction: ParsedTransaction<TNumber> = await coin.parseTransaction<TNumber>({
@@ -64,7 +64,7 @@ export async function verifyTransaction<TNumber extends bigint | number>(
6464
if (!_.isEmpty(keySignatures)) {
6565
const verify = (key, pub) => {
6666
if (!keychains.user || !keychains.user.pub) {
67-
throw new Error('missing user keychain');
67+
throw new SuspiciousTransactionError('missing user keychain');
6868
}
6969
return verifyKeySignature({
7070
userKeychain: keychains.user as { pub: string },
@@ -75,7 +75,7 @@ export async function verifyTransaction<TNumber extends bigint | number>(
7575
const isBackupKeySignatureValid = verify(keychains.backup, keySignatures.backupPub);
7676
const isBitgoKeySignatureValid = verify(keychains.bitgo, keySignatures.bitgoPub);
7777
if (!isBackupKeySignatureValid || !isBitgoKeySignatureValid) {
78-
throw new Error('secondary public key signatures invalid');
78+
throw new SuspiciousTransactionError('secondary public key signatures invalid');
7979
}
8080
debug('successfully verified backup and bitgo key signatures');
8181
} else if (!disableNetworking) {
@@ -86,11 +86,13 @@ export async function verifyTransaction<TNumber extends bigint | number>(
8686

8787
if (parsedTransaction.needsCustomChangeKeySignatureVerification) {
8888
if (!keychains.user || !userPublicKeyVerified) {
89-
throw new Error('transaction requires verification of user public key, but it was unable to be verified');
89+
throw new SuspiciousTransactionError(
90+
'transaction requires verification of user public key, but it was unable to be verified'
91+
);
9092
}
9193
const customChangeKeySignaturesVerified = verifyCustomChangeKeySignatures(parsedTransaction, keychains.user);
9294
if (!customChangeKeySignaturesVerified) {
93-
throw new Error(
95+
throw new SuspiciousTransactionError(
9496
'transaction requires verification of custom change key signatures, but they were unable to be verified'
9597
);
9698
}
@@ -100,7 +102,7 @@ export async function verifyTransaction<TNumber extends bigint | number>(
100102
const missingOutputs = parsedTransaction.missingOutputs;
101103
if (missingOutputs.length !== 0) {
102104
// there are some outputs in the recipients list that have not made it into the actual transaction
103-
throw new Error('expected outputs missing in transaction prebuild');
105+
throw new SuspiciousTransactionError('expected outputs missing in transaction prebuild');
104106
}
105107

106108
const intendedExternalSpend = parsedTransaction.explicitExternalSpendAmount;
@@ -140,13 +142,13 @@ export async function verifyTransaction<TNumber extends bigint | number>(
140142
} else {
141143
// the additional external outputs can only be BitGo's pay-as-you-go fee, but we cannot verify the wallet address
142144
// there are some addresses that are outside the scope of intended recipients that are not change addresses
143-
throw new Error('prebuild attempts to spend to unintended external recipients');
145+
throw new SuspiciousTransactionError('prebuild attempts to spend to unintended external recipients');
144146
}
145147
}
146148

147149
const allOutputs = parsedTransaction.outputs;
148150
if (!txPrebuild.txHex) {
149-
throw new Error(`txPrebuild.txHex not set`);
151+
throw new SuspiciousTransactionError(`txPrebuild.txHex not set`);
150152
}
151153
const inputs = isPsbt
152154
? getPsbtTxInputs(txPrebuild.txHex, coin.network).map((v) => ({
@@ -163,7 +165,7 @@ export async function verifyTransaction<TNumber extends bigint | number>(
163165
const fee = inputAmount - outputAmount;
164166

165167
if (fee < 0) {
166-
throw new Error(
168+
throw new SuspiciousTransactionError(
167169
`attempting to spend ${outputAmount} satoshis, which exceeds the input amount (${inputAmount} satoshis) by ${-fee}`
168170
);
169171
}

modules/sdk-core/src/bitgo/errors.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,12 @@ export class InvalidTransactionError extends BitGoJsError {
160160
}
161161
}
162162

163+
export class SuspiciousTransactionError extends BitGoJsError {
164+
public constructor(message?: string) {
165+
super(message || 'Suspicious transaction detected');
166+
}
167+
}
168+
163169
export class MissingEncryptedKeychainError extends Error {
164170
public constructor(message?: string) {
165171
super(message || 'No encrypted keychains on this wallet.');

0 commit comments

Comments
 (0)