Skip to content

Commit 489974e

Browse files
committed
fix(bitgo): added data validation on tx
Ticket: SC-1436
1 parent cd82add commit 489974e

File tree

2 files changed

+88
-42
lines changed

2 files changed

+88
-42
lines changed

modules/bitgo/test/v2/unit/staking/stakingWalletNonTSS.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ describe('non-TSS Staking Wallet', function () {
400400
await topethWctStakingWallet.buildAndSign({ walletPassphrase: 'passphrase' }, stakingTransaction);
401401
});
402402

403-
it('should log an error if a transaction is failed to be validated transaction if unsigned transaction does not match the staking transaction', async function () {
403+
it('should throw an error if unsigned transaction does not match the staking transaction', async function () {
404404
const unsignedTransaction: PrebuildTransactionResult = {
405405
walletId: topethWctStakingWallet.walletId,
406406
...opethFixtures.unsignedStakingTransaction,
@@ -409,8 +409,6 @@ describe('non-TSS Staking Wallet', function () {
409409
} as PrebuildTransactionResult;
410410
const stakingTransaction: StakingTransaction = opethFixtures.updatedStakingRequest;
411411

412-
const consoleStub = sinon.stub(console, 'error');
413-
414412
nock(microservicesUri)
415413
.get(
416414
`/api/staking/v1/${topethWctStakingWallet.coin}/wallets/${topethWctStakingWallet.walletId}/requests/${stakingTransaction.stakingRequestId}/transactions/${stakingTransaction.id}`
@@ -426,11 +424,14 @@ describe('non-TSS Staking Wallet', function () {
426424
.post(`/api/v2/topeth/wallet/${topethWctStakingWallet.walletId}/tx/build`)
427425
.reply(200, unsignedTransaction);
428426

429-
await topethWctStakingWallet.buildAndSign({ walletPassphrase: 'passphrase' }, stakingTransaction);
427+
const expectedErrorMessage =
428+
'Staking transaction validation failed before signing: ' +
429+
'Unexpected recipient address found in built transaction: 0x86bb6dca2cd6f9a0189c478bbb8f7ee2fef07c89; ' +
430+
'Expected recipient address not found in built transaction: 0x75bb6dca2cd6f9a0189c478bbb8f7ee2fef07c78';
430431

431-
consoleStub.calledWith(
432-
'Invalid recipient address: 0x86bb6dca2cd6f9a0189c478bbb8f7ee2fef07c89, Missing recipient address(es): 0x75bb6dca2cd6f9a0189c478bbb8f7ee2fef07c78'
433-
);
432+
await topethWctStakingWallet
433+
.buildAndSign({ walletPassphrase: 'passphrase' }, stakingTransaction)
434+
.should.be.rejectedWith(expectedErrorMessage);
434435
});
435436
});
436437
});

modules/sdk-core/src/bitgo/staking/stakingWallet.ts

Lines changed: 80 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ export class StakingWallet implements IStakingWallet {
271271
// default to verifying a transaction unless explicitly skipped
272272
const skipVerification = signOptions.transactionVerificationOptions?.skipTransactionVerification ?? false;
273273
if (!isStakingTxRequestPrebuildResult(builtTx.result) && !skipVerification) {
274-
await this.validateBuiltStakingTransaction(transaction, builtTx);
274+
await this.validateBuiltStakingTransaction(builtTx.transaction, builtTx);
275275
}
276276
return await this.sign(signOptions, builtTx);
277277
}
@@ -371,55 +371,100 @@ export class StakingWallet implements IStakingWallet {
371371
}
372372

373373
const explainedTransaction = await coin.explainTransaction(result);
374+
const mismatchErrors: string[] = [];
374375

375-
if (buildParams?.recipients) {
376+
if (buildParams?.recipients && buildParams.recipients.length > 0) {
376377
const userRecipientMap = new Map(
377378
buildParams.recipients.map((recipient) => [recipient.address.toLowerCase(), recipient])
378379
);
379380
const platformRecipientMap = new Map(
380381
(explainedTransaction?.outputs ?? []).map((recipient) => [recipient.address.toLowerCase(), recipient])
381382
);
382383

383-
const mismatchErrors: string[] = [];
384-
385-
for (const [recipientAddress, recipientInfo] of platformRecipientMap) {
386-
if (userRecipientMap.has(recipientAddress)) {
387-
const userRecipient = userRecipientMap.get(recipientAddress);
388-
if (!userRecipient) {
389-
console.error('Unable to determine recipient address');
390-
return;
391-
}
392-
const matchResult = transactionRecipientsMatch(userRecipient, recipientInfo);
393-
if (!matchResult.exactMatch) {
394-
if (!matchResult.tokenMatch) {
395-
mismatchErrors.push(
396-
`Invalid token ${recipientInfo.tokenName} transfer with amount ${recipientInfo.amount} to ${recipientInfo.address} found in built transaction, specified ${userRecipient.tokenName}`
397-
);
398-
}
399-
if (!matchResult.amountMatch) {
400-
mismatchErrors.push(
401-
`Invalid recipient amount for ${recipientInfo.address}, specified ${userRecipient.amount} got ${recipientInfo.amount}`
402-
);
403-
}
404-
}
405-
} else {
406-
mismatchErrors.push(`Invalid recipient address: ${recipientAddress}`);
384+
for (const [address] of platformRecipientMap) {
385+
if (!userRecipientMap.has(address)) {
386+
mismatchErrors.push(`Unexpected recipient address found in built transaction: ${address}`);
407387
}
408388
}
409389

410-
const missingRecipientAddresses = Array.from(userRecipientMap.keys()).filter(
411-
(address) => !platformRecipientMap.has(address)
390+
for (const [address, userRecipient] of userRecipientMap) {
391+
const platformRecipient = platformRecipientMap.get(address);
392+
if (!platformRecipient) {
393+
mismatchErrors.push(`Expected recipient address not found in built transaction: ${address}`);
394+
continue;
395+
}
396+
397+
const matchResult = transactionRecipientsMatch(userRecipient, platformRecipient);
398+
399+
if (!matchResult.amountMatch) {
400+
mismatchErrors.push(
401+
`Recipient ${address} amount mismatch. Expected: ${userRecipient.amount}, Got: ${platformRecipient.amount}`
402+
);
403+
}
404+
if (!matchResult.tokenMatch) {
405+
mismatchErrors.push(
406+
`Recipient ${address} token mismatch. Expected: ${userRecipient.tokenName ?? 'native coin'}, Got: ${
407+
platformRecipient.tokenName ?? 'native coin'
408+
}`
409+
);
410+
}
411+
}
412+
}
413+
if (buildParams?.memo && (explainedTransaction as any).memo !== buildParams.memo) {
414+
mismatchErrors.push(
415+
`Memo mismatch. Expected: '${JSON.stringify(buildParams.memo)}', Got: '${JSON.stringify(
416+
(explainedTransaction as any).memo
417+
)}'`
418+
);
419+
}
420+
421+
if (buildParams?.gasLimit && String((explainedTransaction as any).gasLimit) !== String(buildParams.gasLimit)) {
422+
mismatchErrors.push(
423+
`Gas Limit mismatch. Expected: ${buildParams.gasLimit}, Got: ${(explainedTransaction as any).gasLimit}`
424+
);
425+
}
426+
427+
if (buildParams?.type && (explainedTransaction as any).type !== buildParams.type) {
428+
mismatchErrors.push(
429+
`Transaction type mismatch. Expected: '${buildParams.type}', Got: '${(explainedTransaction as any).type}'`
412430
);
431+
}
413432

414-
if (missingRecipientAddresses.length > 0) {
415-
mismatchErrors.push(`Missing recipient address(es): ${missingRecipientAddresses.join(', ')}`);
433+
if (buildParams?.solInstructions) {
434+
if (
435+
JSON.stringify((explainedTransaction as any).solInstructions) !== JSON.stringify(buildParams.solInstructions)
436+
) {
437+
mismatchErrors.push(
438+
`Solana instructions mismatch. Expected: ${JSON.stringify(
439+
buildParams.solInstructions
440+
)}, Got: ${JSON.stringify((explainedTransaction as any).solInstructions)}`
441+
);
416442
}
417-
if (mismatchErrors.length > 0) {
418-
console.error(mismatchErrors.join(', '));
419-
return;
443+
}
444+
445+
if (buildParams?.aptosCustomTransactionParams) {
446+
if (
447+
JSON.stringify((explainedTransaction as any).aptosCustomTransactionParams) !==
448+
JSON.stringify(buildParams.aptosCustomTransactionParams)
449+
) {
450+
mismatchErrors.push(
451+
`Aptos custom transaction parameters mismatch. Expected: ${JSON.stringify(
452+
buildParams.aptosCustomTransactionParams
453+
)}, Got: ${JSON.stringify((explainedTransaction as any).aptosCustomTransactionParams)}`
454+
);
420455
}
421-
} else {
422-
debug(`Cannot validate staking transaction ${transaction.stakingRequestId} without specified build params`);
456+
}
457+
458+
if (mismatchErrors.length > 0) {
459+
const errorMessage = `Staking transaction validation failed before signing: ${mismatchErrors.join('; ')}`;
460+
debug(errorMessage);
461+
throw new Error(errorMessage);
462+
}
463+
464+
if (!buildParams) {
465+
debug(
466+
`Cannot perform deep validation for staking transaction ${transaction.stakingRequestId} without specified build params`
467+
);
423468
}
424469
}
425470
}

0 commit comments

Comments
 (0)