Skip to content

Commit fa39c0f

Browse files
feat(sdk-coin-xrp): blind signing guards for xrp token enablements
TICKET: WP-5820
1 parent a595ab9 commit fa39c0f

File tree

3 files changed

+574
-40
lines changed

3 files changed

+574
-40
lines changed

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

Lines changed: 107 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,16 @@ import {
2121
ParseTransactionOptions,
2222
promiseProps,
2323
TokenEnablementConfig,
24+
TransactionParams,
2425
UnexpectedAddressError,
2526
VerifyTransactionOptions,
2627
} from '@bitgo/sdk-core';
27-
import { BaseCoin as StaticsBaseCoin, coins, XrpCoin } from '@bitgo/statics';
28+
import { coins, BaseCoin as StaticsBaseCoin, XrpCoin } from '@bitgo/statics';
2829
import * as rippleBinaryCodec from 'ripple-binary-codec';
2930
import * as rippleKeypairs from 'ripple-keypairs';
3031
import * as xrpl from 'xrpl';
3132

33+
import { TokenTransferBuilder, TransactionBuilderFactory, TransferBuilder } from './lib';
3234
import {
3335
ExplainTransactionOptions,
3436
FeeInfo,
@@ -40,11 +42,11 @@ import {
4042
SupplementGenerateWalletOptions,
4143
TransactionExplanation,
4244
VerifyAddressOptions,
45+
XrpTransactionType,
4346
} from './lib/iface';
4447
import { KeyPair as XrpKeyPair } from './lib/keyPair';
4548
import utils from './lib/utils';
4649
import ripple from './ripple';
47-
import { TokenTransferBuilder, TransactionBuilderFactory, TransferBuilder } from './lib';
4850

4951
export class Xrp extends BaseCoin {
5052
protected _staticsCoin: Readonly<StaticsBaseCoin>;
@@ -298,19 +300,121 @@ export class Xrp extends BaseCoin {
298300
};
299301
}
300302

303+
getTransactionTypeRawTxHex(txHex: string): XrpTransactionType | undefined {
304+
let transaction;
305+
if (!txHex) {
306+
throw new Error('missing required param txHex');
307+
}
308+
try {
309+
transaction = rippleBinaryCodec.decode(txHex);
310+
} catch (e) {
311+
try {
312+
transaction = JSON.parse(txHex);
313+
} catch (e) {
314+
throw new Error('txHex needs to be either hex or JSON string for XRP');
315+
}
316+
}
317+
318+
return transaction.TransactionType;
319+
}
320+
321+
verifyTxType(txPrebuildDecoded: TransactionExplanation, txHexPrebuild: string | undefined): void {
322+
if (!txHexPrebuild) throw new Error('Missing txHexPrebuild to verify token type for enabletoken tx');
323+
const transactionType = this.getTransactionTypeRawTxHex(txHexPrebuild);
324+
if (transactionType === undefined) throw new Error('Missing TransactionType on token enablement tx');
325+
if (transactionType !== XrpTransactionType.TrustSet)
326+
throw new Error(`tx type ${transactionType} does not match expected type TrustSet`);
327+
// decoded payload type could come as undefined or any of the enabletoken like types but never as something else like Send, etc
328+
const actualTypeFromDecoded =
329+
'type' in txPrebuildDecoded && typeof txPrebuildDecoded.type === 'string' ? txPrebuildDecoded.type : undefined;
330+
if (
331+
!actualTypeFromDecoded ||
332+
actualTypeFromDecoded === 'enabletoken' ||
333+
actualTypeFromDecoded === 'AssociatedTokenAccountInitialization'
334+
)
335+
return;
336+
337+
throw new Error(`tx type ${actualTypeFromDecoded} does not match the expected type enabletoken`);
338+
}
339+
340+
verifyTokenName(
341+
txParams: TransactionParams,
342+
txPrebuildDecoded: TransactionExplanation,
343+
txHexPrebuild: string | undefined,
344+
coinConfig: XrpCoin
345+
): void {
346+
if (!txHexPrebuild) throw new Error('Missing txHexPrebuild param required for token enablement.');
347+
if (!txParams.recipients || txParams.recipients.length === 0)
348+
throw new Error('Missing recipients param for token enablement.');
349+
const fullTokenName = txParams.recipients[0].tokenName;
350+
if (fullTokenName === undefined)
351+
throw new Error('Param tokenName is required for token enablement. Recipient must include a token name.');
352+
353+
if (!('limitAmount' in txPrebuildDecoded)) throw new Error('Missing limitAmount param for token enablement.');
354+
355+
// we check currency on both the txHex but also the explained payload
356+
const expectedCurrency = utils.getXrpCurrencyFromTokenName(fullTokenName).currency;
357+
if (coinConfig.isToken && expectedCurrency !== txPrebuildDecoded.limitAmount.currency)
358+
throw new Error('Invalid token issuer or currency on token enablement tx');
359+
}
360+
361+
verifyActivationAddress(txParams: TransactionParams, txPrebuildDecoded: TransactionExplanation): void {
362+
if (txParams.recipients === undefined || txParams.recipients.length === 0)
363+
throw new Error('Missing recipients param for token enablement.');
364+
365+
if (txParams.recipients?.length !== 1) {
366+
throw new Error(
367+
`${this.getChain()} doesn't support sending to more than 1 destination address within a single transaction. Try again, using only a single recipient.`
368+
);
369+
}
370+
if (!('account' in txPrebuildDecoded)) throw new Error('missing account on token enablement tx');
371+
372+
const activationAddress = txParams.recipients[0].address;
373+
const accountAddress = txPrebuildDecoded.account;
374+
if (activationAddress !== accountAddress) throw new Error("Account address doesn't match with activation address.");
375+
}
376+
377+
verifyTokenIssuer(txParams: TransactionParams, txPrebuildDecoded: TransactionExplanation): void {
378+
if (txPrebuildDecoded === undefined || !('limitAmount' in txPrebuildDecoded))
379+
throw new Error('missing token issuer on token enablement tx');
380+
const { issuer, currency } = txPrebuildDecoded.limitAmount;
381+
if (!utils.getXrpToken(issuer, currency))
382+
throw new Error('Invalid token issuer or currency on token enablement tx');
383+
}
384+
385+
verifyRequiredKeys(txParams: TransactionParams, txPrebuildDecoded: TransactionExplanation): void {
386+
if (
387+
!('account' in txPrebuildDecoded) ||
388+
!('limitAmount' in txPrebuildDecoded) ||
389+
!txPrebuildDecoded.limitAmount.currency
390+
) {
391+
throw new Error('Explanation is missing required keys (account or limitAmount with currency)');
392+
}
393+
}
394+
301395
/**
302396
* Verify that a transaction prebuild complies with the original intention
303397
* @param txParams params object passed to send
304398
* @param txPrebuild prebuild object returned by server
305399
* @param wallet
306400
* @returns {boolean}
307401
*/
308-
public async verifyTransaction({ txParams, txPrebuild }: VerifyTransactionOptions): Promise<boolean> {
402+
public async verifyTransaction({ txParams, txPrebuild, verification }: VerifyTransactionOptions): Promise<boolean> {
309403
const coinConfig = coins.get(this.getChain()) as XrpCoin;
310404
const explanation = await this.explainTransaction({
311405
txHex: txPrebuild.txHex,
312406
});
313407

408+
// Explaining a tx strips out certain data, for extra measurement we're checking vs the explained tx
409+
// but also vs the tx pre explained.
410+
if (txParams.type === 'enabletoken' && verification?.verifyTokenEnablement) {
411+
this.verifyTxType(explanation, txPrebuild.txHex);
412+
this.verifyActivationAddress(txParams, explanation);
413+
this.verifyTokenIssuer(txParams, explanation);
414+
this.verifyTokenName(txParams, explanation, txPrebuild.txHex, coinConfig);
415+
this.verifyRequiredKeys(txParams, explanation);
416+
}
417+
314418
const output = [...explanation.outputs, ...explanation.changeOutputs][0];
315419
const expectedOutput = txParams.recipients && txParams.recipients[0];
316420

@@ -331,32 +435,6 @@ export class Xrp extends BaseCoin {
331435
throw new Error('transaction prebuild does not match expected output');
332436
}
333437

334-
if (txParams.type === 'enabletoken') {
335-
if (txParams.recipients?.length !== 1) {
336-
throw new Error(
337-
`${this.getChain()} doesn't support sending to more than 1 destination address within a single transaction. Try again, using only a single recipient.`
338-
);
339-
}
340-
const recipient = txParams.recipients[0];
341-
if (!recipient.tokenName) {
342-
throw new Error('Recipient must include a token name.');
343-
}
344-
const recipientCurrency = utils.getXrpCurrencyFromTokenName(recipient.tokenName).currency;
345-
if (coinConfig.isToken) {
346-
if (recipientCurrency !== coinConfig.currencyCode) {
347-
throw new Error('Incorrect token name specified in recipients');
348-
}
349-
}
350-
if (!('account' in explanation) || !('limitAmount' in explanation) || !explanation.limitAmount.currency) {
351-
throw new Error('Explanation is missing required keys (account or limitAmount with currency)');
352-
}
353-
const baseAddress = explanation.account;
354-
const currency = explanation.limitAmount.currency;
355-
356-
if (recipient.address !== baseAddress || recipientCurrency !== currency) {
357-
throw new Error('Tx outputs does not match with expected txParams recipients');
358-
}
359-
}
360438
return true;
361439
}
362440

0 commit comments

Comments
 (0)