diff --git a/modules/sdk-coin-near/src/near.ts b/modules/sdk-coin-near/src/near.ts index b28c541886..6876b2b255 100644 --- a/modules/sdk-coin-near/src/near.ts +++ b/modules/sdk-coin-near/src/near.ts @@ -991,7 +991,7 @@ export class Near extends BaseCoin { async verifyTransaction(params: VerifyTransactionOptions): Promise { let totalAmount = new BigNumber(0); const coinConfig = coins.get(this.getChain()); - const { txPrebuild: txPrebuild, txParams: txParams } = params; + const { txPrebuild: txPrebuild, txParams: txParams, wallet } = params; const transaction = new Transaction(coinConfig); const rawTx = txPrebuild.txHex; if (!rawTx) { @@ -1059,6 +1059,13 @@ export class Near extends BaseCoin { } } + if (params.verification?.consolidationToBaseAddress) { + if (!wallet?.coinSpecific()?.rootAddress) { + throw new Error('Unable to determine base address for consolidation'); + } + await this.verifyConsolidationToBaseAddress(explainedTx, wallet.coinSpecific()?.rootAddress as string); + } + return true; } @@ -1277,4 +1284,15 @@ export class Near extends BaseCoin { throw new Error(`Invalid transaction type on token enablement: expected "${expectedType}", got "${actualType}".`); } } + + protected async verifyConsolidationToBaseAddress( + explainedTx: TransactionExplanation, + baseAddress: string + ): Promise { + for (const output of explainedTx.outputs) { + if (output.address !== baseAddress) { + throw new Error('tx outputs does not match with expected address'); + } + } + } } diff --git a/modules/sdk-coin-near/src/nep141Token.ts b/modules/sdk-coin-near/src/nep141Token.ts index c2dad36a9e..14955194d2 100644 --- a/modules/sdk-coin-near/src/nep141Token.ts +++ b/modules/sdk-coin-near/src/nep141Token.ts @@ -66,7 +66,7 @@ export class Nep141Token extends Near { } async verifyTransaction(params: VerifyTransactionOptions): Promise { - const { txPrebuild: txPrebuild, txParams: txParams } = params; + const { txPrebuild: txPrebuild, txParams: txParams, wallet } = params; const rawTx = txPrebuild.txHex; let totalAmount = new BigNumber(0); if (!rawTx) { @@ -105,6 +105,13 @@ export class Nep141Token extends Near { } } + if (params.verification?.consolidationToBaseAddress) { + if (!wallet?.coinSpecific()?.rootAddress) { + throw new Error('Unable to determine base address for consolidation'); + } + await this.verifyConsolidationToBaseAddress(explainedTx, wallet.coinSpecific()?.rootAddress as string); + } + return true; } } diff --git a/modules/sdk-coin-near/test/unit/near.ts b/modules/sdk-coin-near/test/unit/near.ts index 5c857593c8..dd8c1cdc1b 100644 --- a/modules/sdk-coin-near/test/unit/near.ts +++ b/modules/sdk-coin-near/test/unit/near.ts @@ -3,10 +3,13 @@ import { randomBytes } from 'crypto'; import should from 'should'; import _ from 'lodash'; import sinon from 'sinon'; +import nock from 'nock'; +import assert from 'assert'; import { BitGoAPI } from '@bitgo/sdk-api'; import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; import { coins } from '@bitgo/statics'; +import { common, TransactionPrebuild, Wallet } from '@bitgo/sdk-core'; import { KeyPair, Near, TNear, Transaction } from '../../src'; import nearUtils from '../../src/lib/utils'; @@ -422,6 +425,194 @@ describe('NEAR:', function () { const validTransaction = await basecoin.verifyTransaction({ txParams, txPrebuild }); validTransaction.should.equal(true); }); + + it('should verify a spoofed consolidation transaction', async function () { + // Set up wallet data + const walletData = { + id: '62e156dbd641c000076bbabe04041a90', + coin: 'tnear', + keys: [ + '5b3424f91bf349930e34017500000000', + '5b3424f91bf349930e34017600000000', + '5b3424f91bf349930e34017700000000', + ], + coinSpecific: { + rootAddress: '3a1b77653ea1705ad297db7abe259953b4ad5d2ecc5b50bee9a486f785dd90db', + }, + multisigType: 'tss', + }; + + const consolidationTx = { + txRequestId: '03fdf51a-28e1-4268-b5be-a4afc030ff64', + walletId: '62e156dbd641c000076bbabe', + txHex: + '400000006562376433623333313166616261653338393062363731383536343838383066663831383766353465623565626665343336646131313338333130396638623500eb7d3b3311fabae3890b67185648880ff8187f54eb5ebfe436da11383109f8b5c8a6d8a6f86100004000000061393465333937306165633436626262313536393331636130393065643735666633616164653439373966666566366437346337326435613234376365393466ad19fa7faa45643200f99a992715a02f9806bcbf0c5737ccdf5d61172d61a4f901000000030038bf94dd153d9d7fa7010000000000', + feeInfo: { + fee: 85332111947887500000, + feeString: '85332111947887500377', + }, + txInfo: { + minerFee: '0', + spendAmount: '1999915088987500000000000', + spendAmounts: [ + { + coinName: 'tnear', + amountString: '1999915088987500000000000', + }, + ], + payGoFee: '0', + outputs: [ + { + address: 'a94e3970aec46bbb156931ca090ed75ff3aade4979ffef6d74c72d5a247ce94f', + value: 1.9999150889875e24, + wallet: '62e156dbd641c000076bbabe', + wallets: ['62e156dbd641c000076bbabe'], + enterprise: '6111785f59548d0007a4d13c', + enterprises: ['6111785f59548d0007a4d13c'], + valueString: '1999915088987500000000000', + coinName: 'tnear', + walletType: 'hot', + walletTypes: ['hot'], + }, + ], + inputs: [ + { + value: 1.9999150889875e24, + address: 'eb7d3b3311fabae3890b67185648880ff8187f54eb5ebfe436da11383109f8b5', + valueString: '1999915088987500000000000', + }, + ], + type: '0', + }, + consolidateId: '68ae77ec62346a69d0aee5a2dda69c8c', + coin: 'tnear', + }; + const bgUrl = common.Environments['mock'].uri; + const walletObj = new Wallet(bitgo, basecoin, walletData); + + nock(bgUrl) + .post('/api/v2/tnear/wallet/62e156dbd641c000076bbabe04041a90/consolidateAccount/build') + .reply(200, [ + { + ...consolidationTx, + txHex: + '400000006139346533393730616563343662626231353639333163613039306564373566663361616465343937396666656636643734633732643561323437636539346600a94e3970aec46bbb156931ca090ed75ff3aade4979ffef6d74c72d5a247ce94f1f3dec154a5700004000000066326137386638303336663861343266383730313962316431646336336131623337623139333365653632646464353365373438633530323266316435373961fb6130e6cc30c926fccde8043ce4e43a810ea63b4d3f93623a54176ff8db1cd001000000030000004a480114169545080000000000', + }, + ]); + + nock(bgUrl) + .get('/api/v2/tnear/key/5b3424f91bf349930e34017500000000') + .reply(200, [ + { + encryptedPrv: 'fakePrv', + }, + ]); + + nock(bgUrl) + .get('/api/v2/tnear/wallet/62e156dbd641c000076bbabe04041a90/addresses?sort=-1&limit=1') + .reply(200, [ + { + address: 'a94e3970aec46bbb156931ca090ed75ff3aade4979ffef6d74c72d5a247ce94f', + }, + ]); + + // Call the function to test + await assert.rejects( + async () => { + await walletObj.sendAccountConsolidations({ + walletPassphrase: 'password', + verification: { + consolidationToBaseAddress: true, + }, + }); + }, + { + message: 'tx outputs does not match with expected address', + } + ); + }); + + it('should verify valid a consolidation transaction', async () => { + // Set up wallet data + const walletData = { + id: '62e156dbd641c000076bbabe04041a90', + coin: 'tnear', + keys: [ + '5b3424f91bf349930e34017500000000', + '5b3424f91bf349930e34017600000000', + '5b3424f91bf349930e34017700000000', + ], + coinSpecific: { + rootAddress: 'a94e3970aec46bbb156931ca090ed75ff3aade4979ffef6d74c72d5a247ce94f', + }, + multisigType: 'tss', + }; + + const consolidationTx = { + txRequestId: '03fdf51a-28e1-4268-b5be-a4afc030ff64', + walletId: '62e156dbd641c000076bbabe', + txHex: + '400000006562376433623333313166616261653338393062363731383536343838383066663831383766353465623565626665343336646131313338333130396638623500eb7d3b3311fabae3890b67185648880ff8187f54eb5ebfe436da11383109f8b5c8a6d8a6f86100004000000061393465333937306165633436626262313536393331636130393065643735666633616164653439373966666566366437346337326435613234376365393466ad19fa7faa45643200f99a992715a02f9806bcbf0c5737ccdf5d61172d61a4f901000000030038bf94dd153d9d7fa7010000000000', + feeInfo: { + fee: 85332111947887500000, + feeString: '85332111947887500377', + }, + txInfo: { + minerFee: '0', + spendAmount: '1999915088987500000000000', + spendAmounts: [ + { + coinName: 'tnear', + amountString: '1999915088987500000000000', + }, + ], + payGoFee: '0', + outputs: [ + { + address: 'a94e3970aec46bbb156931ca090ed75ff3aade4979ffef6d74c72d5a247ce94f', + value: 1.9999150889875e24, + wallet: '62e156dbd641c000076bbabe', + wallets: ['62e156dbd641c000076bbabe'], + enterprise: '6111785f59548d0007a4d13c', + enterprises: ['6111785f59548d0007a4d13c'], + valueString: '1999915088987500000000000', + coinName: 'tnear', + walletType: 'hot', + walletTypes: ['hot'], + }, + ], + inputs: [ + { + value: 1.9999150889875e24, + address: 'eb7d3b3311fabae3890b67185648880ff8187f54eb5ebfe436da11383109f8b5', + valueString: '1999915088987500000000000', + }, + ], + type: '0', + }, + consolidateId: '68ae77ec62346a69d0aee5a2dda69c8c', + coin: 'tnear', + }; + try { + if ( + !(await basecoin.verifyTransaction({ + blockhash: '', + feePayer: '', + txParams: {}, + txPrebuild: consolidationTx as unknown as TransactionPrebuild, + walletType: 'tss', + wallet: new Wallet(bitgo, basecoin, walletData), + verification: { + consolidationToBaseAddress: true, + }, + })) + ) { + assert.fail('Transaction should pass verification'); + } + } catch (e) { + assert.fail('Transaction should pass verification'); + } + }); }); describe('Explain Transactions:', () => { diff --git a/modules/sdk-coin-near/test/unit/nep141Token.ts b/modules/sdk-coin-near/test/unit/nep141Token.ts index 0fb00695d7..cfdee808f4 100644 --- a/modules/sdk-coin-near/test/unit/nep141Token.ts +++ b/modules/sdk-coin-near/test/unit/nep141Token.ts @@ -1,10 +1,67 @@ import { BitGoAPI } from '@bitgo/sdk-api'; -import { ITransactionRecipient, Wallet } from '@bitgo/sdk-core'; +import { common, ITransactionRecipient, TransactionPrebuild, Wallet } from '@bitgo/sdk-core'; import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; import { Nep141Token } from '../../src'; import * as testData from '../resources/near'; +import nock from 'nock'; +import assert from 'assert'; +/** + * [ + * { + * "txRequestId": "485fa500-d1c7-4c68-8c9e-050578638dad", + * "walletId": "62e156dbd641c000076bbabe", + * "txHex": "400000006562376433623333313166616261653338393062363731383536343838383066663831383766353465623565626665343336646131313338333130396638623500eb7d3b3311fabae3890b67185648880ff8187f54eb5ebfe436da11383109f8b5cea6d8a6f86100004000000033653232313065313138346234356236346338613433346330613765376232336363303465613765623761366333633332353230643033643461666362386166acf71fa4e1ff78cfe2b34359efc8c0085d8110d5f85649c4a48edb8976a7df8b01000000020b00000066745f7472616e73666572660000007b22616d6f756e74223a223130303030303030222c2272656365697665725f6964223a2261393465333937306165633436626262313536393331636130393065643735666633616164653439373966666566366437346337326435613234376365393466227d00e057eb481b000001000000000000000000000000000000", + * "feeInfo": { + * "fee": 297990720389700000000, + * "feeString": "297990720389700000000" + * }, + * "txInfo": { + * "minerFee": "0", + * "spendAmount": "10000000", + * "spendAmounts": [ + * { + * "coinName": "tnear:usdc", + * "amountString": "10000000" + * } + * ], + * "payGoFee": "0", + * "outputs": [ + * { + * "address": "a94e3970aec46bbb156931ca090ed75ff3aade4979ffef6d74c72d5a247ce94f", + * "value": 10000000, + * "wallet": "62e156dbd641c000076bbabe", + * "wallets": [ + * "62e156dbd641c000076bbabe" + * ], + * "enterprise": "6111785f59548d0007a4d13c", + * "enterprises": [ + * "6111785f59548d0007a4d13c" + * ], + * "valueString": "10000000", + * "coinName": "tnear:usdc", + * "walletType": "hot", + * "walletTypes": [ + * "hot" + * ] + * } + * ], + * "inputs": [ + * { + * "value": 10000000, + * "address": "eb7d3b3311fabae3890b67185648880ff8187f54eb5ebfe436da11383109f8b5", + * "valueString": "10000000" + * } + * ], + * "type": "6" + * }, + * "consolidateId": "68af1af09a63756a636abe8cc65859cc", + * "coin": "tnear", + * "token": "tnear:usdc" + * } + * ] + */ describe('Nep141Token', () => { const nep141TokenName = 'tnear:tnep24dp'; let bitgo: TestBitGoAPI; @@ -108,5 +165,193 @@ describe('Nep141Token', () => { .verifyTransaction({ txParams, txPrebuild, wallet }) .should.rejectedWith('Tx outputs does not match with expected txParams recipients'); }); + + it('should verify spoofed consolidation transaction', async function () { + // Set up wallet data + const walletData = { + id: '62e156dbd641c000076bbabe04041a90', + coin: 'tnear', + keys: [ + '5b3424f91bf349930e34017500000000', + '5b3424f91bf349930e34017600000000', + '5b3424f91bf349930e34017700000000', + ], + coinSpecific: { + rootAddress: '3a1b77653ea1705ad297db7abe259953b4ad5d2ecc5b50bee9a486f785dd90db', + }, + multisigType: 'tss', + }; + + const consolidationTx = { + txRequestId: '485fa500-d1c7-4c68-8c9e-050578638dad', + walletId: '62e156dbd641c000076bbabe', + txHex: + '400000006562376433623333313166616261653338393062363731383536343838383066663831383766353465623565626665343336646131313338333130396638623500eb7d3b3311fabae3890b67185648880ff8187f54eb5ebfe436da11383109f8b5cea6d8a6f86100004000000033653232313065313138346234356236346338613433346330613765376232336363303465613765623761366333633332353230643033643461666362386166acf71fa4e1ff78cfe2b34359efc8c0085d8110d5f85649c4a48edb8976a7df8b01000000020b00000066745f7472616e73666572660000007b22616d6f756e74223a223130303030303030222c2272656365697665725f6964223a2261393465333937306165633436626262313536393331636130393065643735666633616164653439373966666566366437346337326435613234376365393466227d00e057eb481b000001000000000000000000000000000000', + feeInfo: { + fee: 297990720389700000000, + feeString: '297990720389700000000', + }, + txInfo: { + minerFee: '0', + spendAmount: '10000000', + spendAmounts: [ + { + coinName: 'tnear:usdc', + amountString: '10000000', + }, + ], + payGoFee: '0', + outputs: [ + { + address: 'a94e3970aec46bbb156931ca090ed75ff3aade4979ffef6d74c72d5a247ce94f', + value: 10000000, + wallet: '62e156dbd641c000076bbabe', + wallets: ['62e156dbd641c000076bbabe'], + enterprise: '6111785f59548d0007a4d13c', + enterprises: ['6111785f59548d0007a4d13c'], + valueString: '10000000', + coinName: 'tnear:usdc', + walletType: 'hot', + walletTypes: ['hot'], + }, + ], + inputs: [ + { + value: 10000000, + address: 'eb7d3b3311fabae3890b67185648880ff8187f54eb5ebfe436da11383109f8b5', + valueString: '10000000', + }, + ], + type: '6', + }, + consolidateId: '68af1af09a63756a636abe8cc65859cc', + coin: 'tnear', + token: 'tnear:usdc', + }; + const bgUrl = common.Environments['mock'].uri; + const walletObj = new Wallet(bitgo, baseCoin, walletData); + + nock(bgUrl) + .post('/api/v2/tnear:tnep24dp/wallet/62e156dbd641c000076bbabe04041a90/consolidateAccount/build') + .reply(200, [ + { + ...consolidationTx, + txHex: + '400000006139346533393730616563343662626231353639333163613039306564373566663361616465343937396666656636643734633732643561323437636539346600a94e3970aec46bbb156931ca090ed75ff3aade4979ffef6d74c72d5a247ce94f223dec154a57000040000000663261373866383033366638613432663837303139623164316463363361316233376231393333656536326464643533653734386335303232663164353739617770e822d797eb15e4b297bc056320a1a8e1538017931ce0d85cdd90d4da300b0100000003000000a1edccce1bc2d3000000000000', + }, + ]); + + nock(bgUrl) + .get('/api/v2/tnear:tnep24dp/key/5b3424f91bf349930e34017500000000') + .reply(200, [ + { + encryptedPrv: 'fakePrv', + }, + ]); + + nock(bgUrl) + .get('/api/v2/tnear:tnep24dp/wallet/62e156dbd641c000076bbabe04041a90/addresses?sort=-1&limit=1') + .reply(200, [ + { + address: 'a94e3970aec46bbb156931ca090ed75ff3aade4979ffef6d74c72d5a247ce94f', + }, + ]); + + // Call the function to test + await assert.rejects( + async () => { + await walletObj.sendAccountConsolidations({ + walletPassphrase: 'password', + verification: { + consolidationToBaseAddress: true, + }, + }); + }, + { + message: 'tx outputs does not match with expected address', + } + ); + }); + + it('should verify valid a consolidation transaction', async () => { + // Set up wallet data + const walletData = { + id: '62e156dbd641c000076bbabe04041a90', + coin: 'tnear', + keys: [ + '5b3424f91bf349930e34017500000000', + '5b3424f91bf349930e34017600000000', + '5b3424f91bf349930e34017700000000', + ], + coinSpecific: { + rootAddress: 'a94e3970aec46bbb156931ca090ed75ff3aade4979ffef6d74c72d5a247ce94f', + }, + multisigType: 'tss', + }; + + const consolidationTx = { + txRequestId: 'd0486dfd-3c7e-4e66-8159-990b4eba4b79', + walletId: '62e156dbd641c000076bbabe', + txHex: + '400000006562376433623333313166616261653338393062363731383536343838383066663831383766353465623565626665343336646131313338333130396638623500eb7d3b3311fabae3890b67185648880ff8187f54eb5ebfe436da11383109f8b5cfa6d8a6f8610000400000003365323231306531313834623435623634633861343334633061376537623233636330346561376562376136633363333235323064303364346166636238616603400ca7f8e8266e2da00774f5860694f8aae83f3d8534fdcb2f7534db6a2c1401000000020b00000066745f7472616e73666572660000007b22616d6f756e74223a223130303030303030222c2272656365697665725f6964223a2261393465333937306165633436626262313536393331636130393065643735666633616164653439373966666566366437346337326435613234376365393466227d00e057eb481b000001000000000000000000000000000000', + feeInfo: { + fee: 297990720389700000000, + feeString: '297990720389700000000', + }, + txInfo: { + minerFee: '0', + spendAmount: '10000000', + spendAmounts: [ + { + coinName: 'tnear:usdc', + amountString: '10000000', + }, + ], + payGoFee: '0', + outputs: [ + { + address: 'a94e3970aec46bbb156931ca090ed75ff3aade4979ffef6d74c72d5a247ce94f', + value: 10000000, + wallet: '62e156dbd641c000076bbabe', + wallets: ['62e156dbd641c000076bbabe'], + enterprise: '6111785f59548d0007a4d13c', + enterprises: ['6111785f59548d0007a4d13c'], + valueString: '10000000', + coinName: 'tnear:usdc', + walletType: 'hot', + walletTypes: ['hot'], + }, + ], + inputs: [ + { + value: 10000000, + address: 'eb7d3b3311fabae3890b67185648880ff8187f54eb5ebfe436da11383109f8b5', + valueString: '10000000', + }, + ], + type: '6', + }, + consolidateId: '68af1e000137c83efb0c3a8daad994f2', + coin: 'tnear', + token: 'tnear:usdc', + }; + try { + if ( + !(await baseCoin.verifyTransaction({ + txParams: {}, + txPrebuild: consolidationTx as unknown as TransactionPrebuild, + walletType: 'tss', + wallet: new Wallet(bitgo, baseCoin, walletData), + verification: { + consolidationToBaseAddress: true, + }, + })) + ) { + assert.fail('Transaction should pass verification'); + } + } catch (e) { + assert.fail('Transaction should pass verification'); + } + }); }); });