Skip to content

Commit b81a0d0

Browse files
committed
feat(sdk-coin-near): validation for token enablement transactions
-Inside, `verifyTransaction` in near.ts of sdk-coin-near, validate the txHex is valid TICKET: WP-5782
1 parent 8531273 commit b81a0d0

File tree

2 files changed

+201
-11
lines changed

2 files changed

+201
-11
lines changed

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

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,17 +1003,8 @@ export class Near extends BaseCoin {
10031003
// users do not input recipients for consolidation requests as they are generated by the server
10041004
if (txParams.recipients !== undefined) {
10051005
if (txParams.type === 'enabletoken') {
1006-
const tokenName = explainedTx.outputs[0].tokenName;
1007-
if (tokenName) {
1008-
const nepToken = nearUtils.getTokenInstanceFromTokenName(tokenName);
1009-
if (nepToken) {
1010-
explainedTx.outputs.forEach((output) => {
1011-
if (output.amount !== nepToken.storageDepositAmount) {
1012-
throw new Error('Storage deposit amount not matching!');
1013-
}
1014-
});
1015-
}
1016-
}
1006+
// Validate that this is a proper token enablement transaction
1007+
this.validateTokenEnablementTransaction(transaction, explainedTx, txParams, rawTx);
10171008
}
10181009

10191010
const filteredRecipients = txParams.recipients?.map((recipient) => {
@@ -1077,4 +1068,43 @@ export class Near extends BaseCoin {
10771068
}
10781069
}
10791070
}
1071+
1072+
/**
1073+
* Validates that the transaction matches what the user expects
1074+
*/
1075+
private validateTokenEnablementTransaction(
1076+
transaction: Transaction,
1077+
explainedTx: TransactionExplanation,
1078+
txParams: VerifyTransactionOptions['txParams'],
1079+
txHex: string
1080+
): void {
1081+
// Parse transaction from hex to validate actual transaction matches what user sees
1082+
const coinConfig = coins.get(this.getChain());
1083+
const freshTx = new Transaction(coinConfig);
1084+
freshTx.fromRawTransaction(txHex);
1085+
const freshTxData = freshTx.toJson();
1086+
const originalTxData = transaction.toJson();
1087+
1088+
// Verify key transaction fields match to prevent tampering
1089+
if (
1090+
freshTxData.signerId !== originalTxData.signerId ||
1091+
freshTxData.receiverId !== originalTxData.receiverId ||
1092+
freshTxData.publicKey !== originalTxData.publicKey ||
1093+
freshTxData.actions.length !== originalTxData.actions.length
1094+
) {
1095+
throw new Error('Transaction hex does not match provided transaction');
1096+
}
1097+
1098+
// Validate addresses match between parameters and explained transaction
1099+
if (txParams.recipients && explainedTx.outputs) {
1100+
const expectedAddresses = txParams.recipients.map((r) => r.address);
1101+
const explainedAddresses = explainedTx.outputs.map((o) => o.address);
1102+
1103+
for (const addr of expectedAddresses) {
1104+
if (!explainedAddresses.includes(addr)) {
1105+
throw new Error(`Address mismatch: ${addr}`);
1106+
}
1107+
}
1108+
}
1109+
}
10801110
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Import the 'should' assertion library for testing
2+
import 'should';
3+
// Import BitGo testing utilities
4+
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
5+
import { BitGoAPI } from '@bitgo/sdk-api';
6+
// Import core types for transaction verification
7+
import { VerifyTransactionOptions } from '@bitgo/sdk-core';
8+
// Import NEAR-specific types and classes
9+
import { TransactionPrebuild, Near } from '../../src/near';
10+
import { TNear as TNearCoin } from '../../src/tnear';
11+
// Import test data containing sample transactions and account info
12+
import * as testData from '../resources/near';
13+
14+
/**
15+
* Test suite for NEAR token enablement validation
16+
*
17+
* Token enablement is a process where users must "enable" a token before they can receive it.
18+
* This involves creating a storage deposit transaction that allocates space on the blockchain
19+
* for the token balance. This test validates that the security checks work correctly.
20+
*/
21+
describe('NEAR Token Enablement Validation', function () {
22+
let bitgo: TestBitGoAPI; // BitGo API instance for testing
23+
let basecoin: Near; // NEAR coin implementation instance
24+
25+
// Setup that runs once before all tests
26+
before(function () {
27+
// Create a test BitGo instance configured for the test environment
28+
bitgo = TestBitGo.decorate(BitGoAPI, { env: 'test' });
29+
// Register the TNEAR (testnet NEAR) coin with BitGo
30+
bitgo.safeRegister('tnear', TNearCoin.createInstance);
31+
// Get the NEAR coin instance we'll use for testing
32+
basecoin = bitgo.coin('tnear') as Near;
33+
});
34+
35+
/**
36+
* Helper function to create valid transaction parameters for token enablement
37+
* @returns Transaction parameters that should pass validation
38+
*/
39+
const createValidTxParams = () => ({
40+
type: 'enabletoken' as const, // Tells BitGo this is a token enablement transaction
41+
recipients: [
42+
{
43+
address: testData.accounts.account1.address, // The account that will receive the token capability
44+
amount: '0', // Token enablement typically has 0 amount (no tokens transferred, just enabling)
45+
tokenName: 'tnear:tnep24dp', // The specific token being enabled (testnet NEP-141 token)
46+
},
47+
],
48+
});
49+
50+
/**
51+
* Helper function to create a transaction prebuild object
52+
* @param txHex - The raw transaction hex string
53+
* @returns A TransactionPrebuild object with the hex and metadata
54+
*/
55+
const createTxPrebuild = (txHex: string): TransactionPrebuild => ({
56+
txHex, // The actual transaction data in hexadecimal format
57+
key: 'test-key', // Public key (placeholder for testing)
58+
blockHash: 'test-block-hash', // Recent block hash (placeholder for testing)
59+
nonce: BigInt(1), // Transaction nonce to prevent replay attacks
60+
});
61+
62+
/**
63+
* TEST 1: Happy Path - Valid Token Enablement Transaction
64+
*
65+
* This test verifies that a properly formed token enablement transaction passes validation.
66+
* It uses a real storage deposit transaction hex from the test data.
67+
*/
68+
it('should validate valid token enablement transaction', async function () {
69+
// Create valid transaction parameters
70+
const txParams = createValidTxParams();
71+
72+
// Create transaction prebuild using a real storage deposit transaction
73+
// This hex represents a NEAR transaction that creates storage space for a token
74+
const txPrebuild = createTxPrebuild(testData.rawTx.selfStorageDeposit.unsigned);
75+
76+
// Prepare the verification options that would be passed to BitGo
77+
const verifyOptions: VerifyTransactionOptions = {
78+
txParams, // What the user thinks they're signing
79+
txPrebuild, // The actual transaction hex from BitGo
80+
wallet: { id: 'test-wallet' } as any, // Mock wallet object
81+
};
82+
83+
// This should NOT throw an error - the transaction should be valid
84+
// The verifyTransaction method will call validateTokenEnablementTransaction internally
85+
await basecoin.verifyTransaction(verifyOptions);
86+
});
87+
88+
/**
89+
* TEST 2: Security Test - Transaction Hex Mismatch Detection
90+
*
91+
* This test verifies that the validation catches when the transaction hex doesn't match
92+
* what the user expects to sign. This is a critical security check to prevent attacks
93+
* where a malicious actor substitutes a different transaction.
94+
*/
95+
it('should reject transaction with mismatched hex', async function () {
96+
// Create token enablement parameters but with the recipient address that matches the transfer transaction
97+
const txParams = {
98+
type: 'enabletoken' as const,
99+
recipients: [
100+
{
101+
address: testData.accounts.account2.address, // This matches the transfer transaction recipient
102+
amount: '0',
103+
tokenName: 'tnear:tnep24dp',
104+
},
105+
],
106+
};
107+
108+
// BUT use a DIFFERENT transaction type (regular transfer instead of storage deposit)
109+
// This simulates an attack where someone tries to trick the user into signing
110+
// a different transaction than what they think they're signing
111+
const txPrebuild = createTxPrebuild(testData.rawTx.transfer.unsigned); // Different transaction type
112+
113+
const verifyOptions: VerifyTransactionOptions = {
114+
txParams, // User thinks they're enabling a token
115+
txPrebuild, // But the actual hex is for a money transfer!
116+
wallet: { id: 'test-wallet' } as any,
117+
};
118+
119+
// This SHOULD throw an error because the hex doesn't match the expected transaction type
120+
// The validation will detect that the transaction outputs don't match the expected token enablement parameters
121+
await basecoin
122+
.verifyTransaction(verifyOptions)
123+
.should.be.rejectedWith('Tx outputs does not match with expected txParams recipients');
124+
});
125+
126+
/**
127+
* TEST 3: Security Test - Address Mismatch Detection
128+
*
129+
* This test verifies that the validation catches when the recipient address in the
130+
* transaction parameters doesn't match the actual address in the transaction hex.
131+
* This prevents attacks where someone changes the destination address.
132+
*/
133+
it('should reject transaction with address mismatch', async function () {
134+
// Create transaction parameters with a WRONG address
135+
const txParams = {
136+
type: 'enabletoken' as const,
137+
recipients: [
138+
{
139+
address: 'wrong.address.near', // This doesn't match the address in the transaction hex
140+
amount: '0',
141+
tokenName: 'tnear:tnep24dp',
142+
},
143+
],
144+
};
145+
146+
// Use the correct storage deposit transaction hex
147+
const txPrebuild = createTxPrebuild(testData.rawTx.selfStorageDeposit.unsigned);
148+
149+
const verifyOptions: VerifyTransactionOptions = {
150+
txParams, // Contains wrong address
151+
txPrebuild, // Contains correct address in the hex
152+
wallet: { id: 'test-wallet' } as any,
153+
};
154+
155+
// This SHOULD throw an error because the addresses don't match
156+
// The validateTokenEnablementTransaction method should detect this mismatch
157+
// and prevent the user from being tricked into enabling tokens for the wrong address
158+
await basecoin.verifyTransaction(verifyOptions).should.be.rejectedWith('Address mismatch: wrong.address.near');
159+
});
160+
});

0 commit comments

Comments
 (0)