Skip to content

Commit 4c6a176

Browse files
authored
Merge pull request #6957 from BitGo/validate-Staking-txrequest
fix(SC-1435): add validation for unsigned txHex matches client staking request
2 parents 39bfb65 + b535375 commit 4c6a176

File tree

2 files changed

+36
-38
lines changed

2 files changed

+36
-38
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: 28 additions & 31 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
}
@@ -372,7 +372,7 @@ export class StakingWallet implements IStakingWallet {
372372

373373
const explainedTransaction = await coin.explainTransaction(result);
374374

375-
if (buildParams?.recipients) {
375+
if (buildParams?.recipients && buildParams.recipients.length > 0) {
376376
const userRecipientMap = new Map(
377377
buildParams.recipients.map((recipient) => [recipient.address.toLowerCase(), recipient])
378378
);
@@ -382,41 +382,38 @@ export class StakingWallet implements IStakingWallet {
382382

383383
const mismatchErrors: string[] = [];
384384

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}`);
385+
for (const [address] of platformRecipientMap) {
386+
if (!userRecipientMap.has(address)) {
387+
mismatchErrors.push(`Unexpected recipient address found in built transaction: ${address}`);
407388
}
408389
}
409390

410-
const missingRecipientAddresses = Array.from(userRecipientMap.keys()).filter(
411-
(address) => !platformRecipientMap.has(address)
412-
);
391+
for (const [address, userRecipient] of userRecipientMap) {
392+
const platformRecipient = platformRecipientMap.get(address);
393+
if (!platformRecipient) {
394+
mismatchErrors.push(`Expected recipient address not found in built transaction: ${address}`);
395+
continue;
396+
}
397+
398+
const matchResult = transactionRecipientsMatch(userRecipient, platformRecipient);
413399

414-
if (missingRecipientAddresses.length > 0) {
415-
mismatchErrors.push(`Missing recipient address(es): ${missingRecipientAddresses.join(', ')}`);
400+
if (!matchResult.amountMatch) {
401+
mismatchErrors.push(
402+
`Recipient ${address} amount mismatch. Expected: ${userRecipient.amount}, Got: ${platformRecipient.amount}`
403+
);
404+
}
405+
if (!matchResult.tokenMatch) {
406+
mismatchErrors.push(
407+
`Recipient ${address} token mismatch. Expected: ${userRecipient.tokenName ?? 'native coin'}, Got: ${
408+
platformRecipient.tokenName ?? 'native coin'
409+
}`
410+
);
411+
}
416412
}
417413
if (mismatchErrors.length > 0) {
418-
console.error(mismatchErrors.join(', '));
419-
return;
414+
const errorMessage = `Staking transaction validation failed before signing: ${mismatchErrors.join('; ')}`;
415+
debug(errorMessage);
416+
throw new Error(errorMessage);
420417
}
421418
} else {
422419
debug(`Cannot validate staking transaction ${transaction.stakingRequestId} without specified build params`);

0 commit comments

Comments
 (0)