Skip to content

Commit ce23bed

Browse files
feat(sdk-coin-xlm): blind signing guards for token enablements
TICKET: WP-5819
1 parent f658c12 commit ce23bed

File tree

3 files changed

+743
-28
lines changed

3 files changed

+743
-28
lines changed

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

Lines changed: 146 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
import assert from 'assert';
2+
import { BigNumber } from 'bignumber.js';
23
import * as _ from 'lodash';
34
import * as querystring from 'querystring';
4-
import * as url from 'url';
5-
import * as request from 'superagent';
65
import * as stellar from 'stellar-sdk';
7-
import { BigNumber } from 'bignumber.js';
8-
import * as Utils from './lib/utils';
6+
import * as request from 'superagent';
7+
import * as url from 'url';
98
import { KeyPair as StellarKeyPair } from './lib/keyPair';
9+
import * as Utils from './lib/utils';
1010

11+
import { toBitgoRequest } from '@bitgo/sdk-api';
1112
import {
13+
AuditDecryptedKeyParams,
1214
BaseCoin,
15+
SignTransactionOptions as BaseSignTransactionOptions,
16+
TransactionExplanation as BaseTransactionExplanation,
17+
TransactionRecipient as BaseTransactionOutput,
18+
TransactionParams as BaseTransactionParams,
19+
TransactionPrebuild as BaseTransactionPrebuild,
20+
VerifyAddressOptions as BaseVerifyAddressOptions,
21+
VerifyTransactionOptions as BaseVerifyTransactionOptions,
1322
BitGoBase,
1423
checkKrsProvider,
1524
common,
@@ -19,26 +28,17 @@ import {
1928
ITransactionRecipient,
2029
KeyIndices,
2130
KeyPair,
31+
MultisigType,
32+
multisigTypes,
33+
NotSupported,
2234
ParsedTransaction,
2335
ParseTransactionOptions,
2436
promiseProps,
25-
SignTransactionOptions as BaseSignTransactionOptions,
2637
StellarFederationUserNotFoundError,
2738
TokenEnablementConfig,
28-
TransactionExplanation as BaseTransactionExplanation,
29-
TransactionParams as BaseTransactionParams,
30-
TransactionPrebuild as BaseTransactionPrebuild,
31-
TransactionRecipient as BaseTransactionOutput,
3239
UnexpectedAddressError,
33-
VerifyAddressOptions as BaseVerifyAddressOptions,
34-
VerifyTransactionOptions as BaseVerifyTransactionOptions,
3540
Wallet,
36-
NotSupported,
37-
MultisigType,
38-
multisigTypes,
39-
AuditDecryptedKeyParams,
4041
} from '@bitgo/sdk-core';
41-
import { toBitgoRequest } from '@bitgo/sdk-api';
4242
import { getStellarKeys } from './getStellarKeys';
4343

4444
/**
@@ -1002,14 +1002,17 @@ export class Xlm extends BaseCoin {
10021002
* @param {TransactionParams} txParams - params used to build the tx
10031003
*/
10041004
verifyEnableTokenTxOperations(operations: stellar.Operation[], txParams: TransactionParams): void {
1005+
// CHECKED on getTrustlineOperationsOrThrow
10051006
const trustlineOperations = _.filter(operations, ['type', 'changeTrust']) as stellar.Operation.ChangeTrust[];
10061007
if (trustlineOperations.length !== _.get(txParams, 'recipients', []).length) {
10071008
throw new Error('transaction prebuild does not match expected trustline operations');
10081009
}
10091010
_.forEach(trustlineOperations, (op: stellar.Operation) => {
1011+
// CHECKED on verifyTxType
10101012
if (op.type !== 'changeTrust') {
10111013
throw new Error('Invalid asset type');
10121014
}
1015+
// CHECKED on verifyAssetType
10131016
if (op.line.getAssetType() === 'liquidity_pool_shares') {
10141017
throw new Error('Invalid asset type');
10151018
}
@@ -1027,16 +1030,42 @@ export class Xlm extends BaseCoin {
10271030
});
10281031
}
10291032

1033+
getTrustlineOperationsOrThrow(
1034+
operations: stellar.Operation[],
1035+
txParams: TransactionParams,
1036+
operationTypePropName: 'trustlines' | 'recipients'
1037+
): stellar.Operation.ChangeTrust[] {
1038+
const trustlineOperations = operations.filter((op) => op?.type === 'changeTrust');
1039+
if (trustlineOperations.length !== _.get(txParams, operationTypePropName, []).length) {
1040+
throw new Error('transaction prebuild does not match expected trustline operations');
1041+
}
1042+
1043+
return trustlineOperations;
1044+
}
1045+
1046+
getTrustlineOperationLineOrThrow(operation: stellar.Operation) {
1047+
if (operation.type === 'changeTrust' && operation.line) return operation.line;
1048+
throw new Error('Invalid operation - expected changeTrust operation with line property');
1049+
}
1050+
1051+
getTrustlineOperationLimitOrThrow(operation: stellar.Operation) {
1052+
if (operation.type === 'changeTrust' && operation.limit) return operation.limit;
1053+
throw new Error('Invalid operation - expected changeTrust operation with limit property');
1054+
}
1055+
1056+
isOperationLineOfAssetType(line: stellar.Asset | stellar.LiquidityPoolAsset): line is stellar.Asset {
1057+
// line should be stellar.Asset, we removed the explicit cast and check the type instead
1058+
if (!line.getAssetType) return false;
1059+
return line.getAssetType() !== 'liquidity_pool_shares';
1060+
}
1061+
10301062
/**
10311063
* Verify that a tx prebuild's operations comply with the original intention
10321064
* @param {stellar.Operation} operations - tx operations
10331065
* @param {TransactionParams} txParams - params used to build the tx
10341066
*/
10351067
verifyTrustlineTxOperations(operations: stellar.Operation[], txParams: TransactionParams): void {
1036-
const trustlineOperations = _.filter(operations, ['type', 'changeTrust']) as stellar.Operation.ChangeTrust[];
1037-
if (trustlineOperations.length !== _.get(txParams, 'trustlines', []).length) {
1038-
throw new Error('transaction prebuild does not match expected trustline operations');
1039-
}
1068+
const trustlineOperations = this.getTrustlineOperationsOrThrow(operations, txParams, 'trustlines');
10401069
_.forEach(trustlineOperations, (op: stellar.Operation) => {
10411070
if (op.type !== 'changeTrust') {
10421071
throw new Error('Invalid asset type');
@@ -1067,6 +1096,95 @@ export class Xlm extends BaseCoin {
10671096
});
10681097
}
10691098

1099+
getRecipientOrThrow(txParams: TransactionParams): ITransactionRecipient {
1100+
if (!txParams.recipients || txParams.recipients.length === 0)
1101+
throw new Error('Missing recipients on token enablement');
1102+
if (txParams.recipients.length > 1) throw new Error('Multiple recipients not supported on token enablement');
1103+
return txParams.recipients[0];
1104+
}
1105+
1106+
getTokenDataOrThrow(txParams: TransactionParams): string {
1107+
const recipient = this.getRecipientOrThrow(txParams);
1108+
const fullTokenData = recipient.tokenName;
1109+
if (!fullTokenData || fullTokenData === '') throw new Error('Missing tokenName on token enablement recipient');
1110+
return fullTokenData;
1111+
}
1112+
1113+
private getTokenCodeFromTokenName(tokenName: string): string {
1114+
const tokenCode = tokenName.split(':')[1]?.split('-')[0] ?? '';
1115+
if (tokenCode === '') throw new Error(`Invalid tokenName format on token enablement for token ${tokenName}`);
1116+
return tokenCode;
1117+
}
1118+
1119+
private getIssuerFromTokenName(tokenName: string): string {
1120+
const issuer = tokenName.split(':')[1]?.split('-')[1] ?? '';
1121+
if (issuer === '') throw new Error(`Invalid issuer format on token enablement for token ${tokenName}`);
1122+
return issuer;
1123+
}
1124+
1125+
// first to verify
1126+
verifyTxType(txParams: TransactionParams, operations: stellar.Operation[]): void {
1127+
operations.forEach((operation) => {
1128+
if (!operation.type) throw new Error('Missing operation type on token enablement');
1129+
if (operation.type !== 'changeTrust')
1130+
throw new Error(`Invalid operation on token enablement: expected changeTrust, got ${operation.type}`);
1131+
});
1132+
}
1133+
1134+
verifyAssetType(txParams: TransactionParams, operations: stellar.Operation[]): void {
1135+
operations.forEach((operation) => {
1136+
const line = this.getTrustlineOperationLineOrThrow(operation);
1137+
const assetType = line.getAssetType();
1138+
if (assetType === 'liquidity_pool_shares')
1139+
throw new Error(`Invalid asset type on token enablement: got ${assetType}`);
1140+
});
1141+
}
1142+
1143+
verifyTokenIssuer(txParams: TransactionParams, operations: stellar.Operation[]): void {
1144+
const fullTokenData = this.getTokenDataOrThrow(txParams);
1145+
const expectedIssuer = this.getIssuerFromTokenName(fullTokenData);
1146+
1147+
operations.forEach((operation) => {
1148+
const line = this.getTrustlineOperationLineOrThrow(operation);
1149+
if (!('issuer' in line)) throw new Error('Missing issuer on token enablement operation');
1150+
if (line.issuer !== expectedIssuer)
1151+
throw new Error(`Invalid issuer on token enablement operation: expected ${expectedIssuer}, got ${line.issuer}`);
1152+
});
1153+
}
1154+
1155+
verifyTokenName(txParams: TransactionParams, operations: stellar.Operation[]): void {
1156+
const fullTokenData = this.getTokenDataOrThrow(txParams);
1157+
const expectedTokenCode = this.getTokenCodeFromTokenName(fullTokenData);
1158+
1159+
operations.forEach((operation) => {
1160+
const line = this.getTrustlineOperationLineOrThrow(operation);
1161+
if (!('code' in line)) throw new Error('Missing token code on token enablement operation');
1162+
if (line.code === '') throw new Error('Empty token code on token enablement operation');
1163+
if (line.code !== expectedTokenCode)
1164+
throw new Error(
1165+
`Invalid token code on token enablement operation: expected ${expectedTokenCode}, got ${line.code}`
1166+
);
1167+
});
1168+
}
1169+
1170+
verifyTokenLimits(txParams: TransactionParams, operations: stellar.Operation[]): void {
1171+
const recipient = this.getRecipientOrThrow(txParams);
1172+
1173+
operations.forEach((operation) => {
1174+
// trustline params use limits in base units
1175+
const line = this.getTrustlineOperationLineOrThrow(operation);
1176+
const limit = this.getTrustlineOperationLimitOrThrow(operation); // line should be stellar.Asset
1177+
if (!this.isOperationLineOfAssetType(line)) throw new Error('Invalid asset type');
1178+
const operationLimitBaseUnits = this.bigUnitsToBaseUnits(limit);
1179+
const operationToken = this.getTokenNameFromStellarAsset(line);
1180+
1181+
// Enable token limit is set to Xlm.maxTrustlineLimit by default
1182+
if (recipient.tokenName !== operationToken || operationLimitBaseUnits !== Xlm.maxTrustlineLimit) {
1183+
throw new Error('Token limit must be set to max limit on enable token operations');
1184+
}
1185+
});
1186+
}
1187+
10701188
/**
10711189
* Verify that a transaction prebuild complies with the original intention
10721190
*
@@ -1099,8 +1217,14 @@ export class Xlm extends BaseCoin {
10991217
(operation) => operation.type === 'createAccount' || operation.type === 'payment'
11001218
);
11011219

1102-
if (txParams.type === 'enabletoken') {
1103-
this.verifyEnableTokenTxOperations(tx.operations, txParams);
1220+
if (txParams.type === 'enabletoken' && verification.verifyTokenEnablement) {
1221+
const trustlineOperations = this.getTrustlineOperationsOrThrow(tx.operations, txParams, 'recipients');
1222+
this.verifyTxType(txParams, trustlineOperations);
1223+
1224+
this.verifyAssetType(txParams, trustlineOperations);
1225+
this.verifyTokenIssuer(txParams, trustlineOperations);
1226+
this.verifyTokenName(txParams, trustlineOperations);
1227+
this.verifyTokenLimits(txParams, trustlineOperations);
11041228
} else if (txParams.type === 'trustline') {
11051229
this.verifyTrustlineTxOperations(tx.operations, txParams);
11061230
} else {

0 commit comments

Comments
 (0)