Skip to content

Commit 9dff461

Browse files
prajwalu142bitgobot
authored andcommitted
fix(sdk-coin-stx): verify recipient and amount in native STX verifyTransaction
Previously, Stx.verifyTransaction was a near no-op: it only checked that the recipient count was <= 1. A compromised prebuild could redirect native STX sends to any address or alter the amount, and local verification would still pass. Port the decode-and-compare pattern from Sip10Token.verifyTransaction into Stx.verifyTransaction, adapted for single-recipient STX. The new implementation decodes the prebuild txHex via explainTransaction and directly compares the single decoded output's address and amount against txParams.recipients[0], then validates the memo. No iteration needed since STX enforces a single-recipient constraint at the top of the method. This closes the gap where the SIP10 token path already had full recipient validation but native STX did not. Ticket: CSHLD-839 Session-Id: 7de30368-fc1b-4b09-9d2e-55be78e462dc Task-Id: 2203e736-35f2-4e2f-8cde-6cdbd0d3e2b6
1 parent 2e81c2d commit 9dff461

2 files changed

Lines changed: 118 additions & 2 deletions

File tree

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

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,12 @@ import { ExplainTransactionOptions, StxSignTransactionOptions, StxTransactionExp
4040
import { StxLib } from '.';
4141
import { TransactionBuilderFactory } from './lib';
4242
import { TransactionBuilder } from './lib/transactionBuilder';
43-
import { findContractTokenNameUsingContract, findTokenNameByContract, getAddressDetails } from './lib/utils';
43+
import {
44+
findContractTokenNameUsingContract,
45+
findTokenNameByContract,
46+
getAddressDetails,
47+
getMemoIdAndBaseAddressFromAddress,
48+
} from './lib/utils';
4449
import {
4550
AddressDetails,
4651
NativeStxBalance,
@@ -98,12 +103,42 @@ export class Stx extends BaseCoin {
98103
}
99104

100105
async verifyTransaction(params: VerifyTransactionOptions): Promise<boolean> {
101-
const { txParams } = params;
106+
const { txPrebuild, txParams } = params;
107+
const { memo } = txParams;
102108
if (Array.isArray(txParams.recipients) && txParams.recipients.length > 1) {
103109
throw new Error(
104110
`${this.getChain()} doesn't support sending to more than 1 destination address within a single transaction. Try again, using only a single recipient.`
105111
);
106112
}
113+
const rawTx = txPrebuild?.txHex;
114+
if (!rawTx) {
115+
throw new Error('missing required tx prebuild property txHex');
116+
}
117+
const explainedTx = await this.explainTransaction({ txHex: rawTx, feeInfo: { fee: '' } });
118+
const recipient = txParams.recipients?.[0];
119+
if (recipient !== undefined && explainedTx) {
120+
const txOutput = explainedTx.outputs[0];
121+
const recipientAddress = getMemoIdAndBaseAddressFromAddress(recipient.address).address;
122+
if (txOutput?.address !== recipientAddress || BigInt(txOutput?.amount) !== BigInt(recipient.amount)) {
123+
throw new Error('Tx outputs does not match with expected txParams recipients');
124+
}
125+
// compare memo
126+
let memoInput = '';
127+
if (memo && memo.value) {
128+
memoInput = memo.value;
129+
} else {
130+
const addressDetails = getMemoIdAndBaseAddressFromAddress(recipient.address);
131+
memoInput = addressDetails.memoId ?? '';
132+
}
133+
const memoOutput = explainedTx.memo ?? '';
134+
if (memoInput !== memoOutput) {
135+
throw new Error('Tx memo does not match with expected txParams recipient memo');
136+
}
137+
// compare amount
138+
if (!new BigNumber(recipient.amount).isEqualTo(explainedTx.outputAmount)) {
139+
throw new Error('Tx total amount does not match with expected total amount field');
140+
}
141+
}
107142
return true;
108143
}
109144

modules/sdk-coin-stx/test/unit/stx.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,8 @@ describe('STX:', function () {
324324
describe('Verify Transaction', function () {
325325
const address1 = '0x174cfd823af8ce27ed0afee3fcf3c3ba259116be';
326326
const address2 = '0x7e85bdc27c050e3905ebf4b8e634d9ad6edd0de6';
327+
const txPrebuild = { txHex: testData.txForExplainTransfer, txInfo: {} };
328+
327329
it('should reject a txPrebuild with more than one recipient', async function () {
328330
const wallet = new Wallet(bitgo, basecoin, {});
329331

@@ -342,6 +344,85 @@ describe('STX:', function () {
342344
`tstx doesn't support sending to more than 1 destination address within a single transaction. Try again, using only a single recipient.`
343345
);
344346
});
347+
348+
it('should reject when txPrebuild is missing txHex', async function () {
349+
const wallet = new Wallet(bitgo, basecoin, {});
350+
const txParams = {
351+
recipients: [{ amount: '1000', address: testData.txExplainedTransfer.recipient }],
352+
wallet,
353+
walletPassphrase: 'fakeWalletPassphrase',
354+
};
355+
await basecoin
356+
.verifyTransaction({ txPrebuild: {}, txParams })
357+
.should.be.rejectedWith('missing required tx prebuild property txHex');
358+
});
359+
360+
it('should succeed to verify a native STX transfer with matching recipient and amount', async function () {
361+
const wallet = new Wallet(bitgo, basecoin, {});
362+
const txParams = {
363+
recipients: [
364+
{
365+
address: testData.txExplainedTransfer.recipient,
366+
amount: testData.txExplainedTransfer.outputAmount,
367+
},
368+
],
369+
memo: { type: '', value: testData.txExplainedTransfer.memo },
370+
wallet,
371+
};
372+
const result = await basecoin.verifyTransaction({ txPrebuild, txParams });
373+
result.should.equal(true);
374+
});
375+
376+
it('should fail to verify transaction with wrong recipient address', async function () {
377+
const wallet = new Wallet(bitgo, basecoin, {});
378+
const txParams = {
379+
recipients: [
380+
{
381+
address: 'ST11NJTTKGVT6D1HY4NJRVQWMQM7TVAR091EJ8P2Y',
382+
amount: testData.txExplainedTransfer.outputAmount,
383+
},
384+
],
385+
memo: { type: '', value: testData.txExplainedTransfer.memo },
386+
wallet,
387+
};
388+
await basecoin
389+
.verifyTransaction({ txPrebuild, txParams })
390+
.should.be.rejectedWith('Tx outputs does not match with expected txParams recipients');
391+
});
392+
393+
it('should fail to verify transaction with wrong amount', async function () {
394+
const wallet = new Wallet(bitgo, basecoin, {});
395+
const txParams = {
396+
recipients: [
397+
{
398+
address: testData.txExplainedTransfer.recipient,
399+
amount: '9999',
400+
},
401+
],
402+
memo: { type: '', value: testData.txExplainedTransfer.memo },
403+
wallet,
404+
};
405+
await basecoin
406+
.verifyTransaction({ txPrebuild, txParams })
407+
.should.be.rejectedWith('Tx outputs does not match with expected txParams recipients');
408+
});
409+
410+
it('should fail to verify transaction with wrong memo', async function () {
411+
const wallet = new Wallet(bitgo, basecoin, {});
412+
const txParams = {
413+
recipients: [
414+
{
415+
address: testData.txExplainedTransfer.recipient,
416+
amount: testData.txExplainedTransfer.outputAmount,
417+
},
418+
],
419+
memo: { type: '', value: 'wrong memo' },
420+
wallet,
421+
};
422+
await basecoin
423+
.verifyTransaction({ txPrebuild, txParams })
424+
.should.be.rejectedWith('Tx memo does not match with expected txParams recipient memo');
425+
});
345426
});
346427

347428
describe('Recover Transaction STX', function () {

0 commit comments

Comments
 (0)