Skip to content

Commit 9d20ae8

Browse files
feat(sdk-coin-xlm): blind signing guards for token enablements
TICKET: WP-5819
1 parent 4a8b2be commit 9d20ae8

File tree

3 files changed

+925
-57
lines changed

3 files changed

+925
-57
lines changed

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

Lines changed: 139 additions & 49 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
/**
@@ -996,35 +996,33 @@ export class Xlm extends BaseCoin {
996996
} as any;
997997
}
998998

999-
/**
1000-
* Verify that a tx prebuild's operations comply with the original intention
1001-
* @param {stellar.Operation} operations - tx operations
1002-
* @param {TransactionParams} txParams - params used to build the tx
1003-
*/
1004-
verifyEnableTokenTxOperations(operations: stellar.Operation[], txParams: TransactionParams): void {
1005-
const trustlineOperations = _.filter(operations, ['type', 'changeTrust']) as stellar.Operation.ChangeTrust[];
1006-
if (trustlineOperations.length !== _.get(txParams, 'recipients', []).length) {
999+
getTrustlineOperationsOrThrow(
1000+
operations: stellar.Operation[],
1001+
txParams: TransactionParams,
1002+
operationTypePropName: 'trustlines' | 'recipients'
1003+
): stellar.Operation.ChangeTrust[] {
1004+
const trustlineOperations = operations.filter((op) => op?.type === 'changeTrust');
1005+
if (trustlineOperations.length !== _.get(txParams, operationTypePropName, []).length) {
10071006
throw new Error('transaction prebuild does not match expected trustline operations');
10081007
}
1009-
_.forEach(trustlineOperations, (op: stellar.Operation) => {
1010-
if (op.type !== 'changeTrust') {
1011-
throw new Error('Invalid asset type');
1012-
}
1013-
if (op.line.getAssetType() === 'liquidity_pool_shares') {
1014-
throw new Error('Invalid asset type');
1015-
}
1016-
const asset = op.line as stellar.Asset;
1017-
const opToken = this.getTokenNameFromStellarAsset(asset);
1018-
const tokenTrustline = _.find(txParams.recipients, (recipient) => {
1019-
// trustline params use limits in base units
1020-
const opLimitBaseUnits = this.bigUnitsToBaseUnits(op.limit);
1021-
// Enable token limit is set to Xlm.maxTrustlineLimit by default
1022-
return recipient.tokenName === opToken && opLimitBaseUnits === Xlm.maxTrustlineLimit;
1023-
});
1024-
if (!tokenTrustline) {
1025-
throw new Error('transaction prebuild does not match expected trustline tokens');
1026-
}
1027-
});
1008+
1009+
return trustlineOperations;
1010+
}
1011+
1012+
getTrustlineOperationLineOrThrow(operation: stellar.Operation): stellar.Asset | stellar.LiquidityPoolAsset {
1013+
if (operation.type === 'changeTrust' && operation.line) return operation.line;
1014+
throw new Error('Invalid operation - expected changeTrust operation with line property');
1015+
}
1016+
1017+
getTrustlineOperationLimitOrThrow(operation: stellar.Operation): string {
1018+
if (operation.type === 'changeTrust' && operation.limit) return operation.limit;
1019+
throw new Error('Invalid operation - expected changeTrust operation with limit property');
1020+
}
1021+
1022+
isOperationLineOfAssetType(line: stellar.Asset | stellar.LiquidityPoolAsset): line is stellar.Asset {
1023+
// line should be stellar.Asset, we removed the explicit cast and check the type instead
1024+
if (!line.getAssetType) return false;
1025+
return line.getAssetType() !== 'liquidity_pool_shares';
10281026
}
10291027

10301028
/**
@@ -1033,10 +1031,7 @@ export class Xlm extends BaseCoin {
10331031
* @param {TransactionParams} txParams - params used to build the tx
10341032
*/
10351033
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-
}
1034+
const trustlineOperations = this.getTrustlineOperationsOrThrow(operations, txParams, 'trustlines');
10401035
_.forEach(trustlineOperations, (op: stellar.Operation) => {
10411036
if (op.type !== 'changeTrust') {
10421037
throw new Error('Invalid asset type');
@@ -1067,6 +1062,95 @@ export class Xlm extends BaseCoin {
10671062
});
10681063
}
10691064

1065+
getRecipientOrThrow(txParams: TransactionParams): ITransactionRecipient {
1066+
if (!txParams.recipients || txParams.recipients.length === 0)
1067+
throw new Error('Missing recipients on token enablement');
1068+
if (txParams.recipients.length > 1) throw new Error('Multiple recipients not supported on token enablement');
1069+
return txParams.recipients[0];
1070+
}
1071+
1072+
getTokenDataOrThrow(txParams: TransactionParams): string {
1073+
const recipient = this.getRecipientOrThrow(txParams);
1074+
const fullTokenData = recipient.tokenName;
1075+
if (!fullTokenData || fullTokenData === '') throw new Error('Missing tokenName on token enablement recipient');
1076+
return fullTokenData;
1077+
}
1078+
1079+
private getTokenCodeFromTokenName(tokenName: string): string {
1080+
const tokenCode = tokenName.split(':')[1]?.split('-')[0] ?? '';
1081+
if (tokenCode === '') throw new Error(`Invalid tokenName format on token enablement for token ${tokenName}`);
1082+
return tokenCode;
1083+
}
1084+
1085+
private getIssuerFromTokenName(tokenName: string): string {
1086+
const issuer = tokenName.split(':')[1]?.split('-')[1] ?? '';
1087+
if (issuer === '') throw new Error(`Invalid issuer format on token enablement for token ${tokenName}`);
1088+
return issuer;
1089+
}
1090+
1091+
// first to verify
1092+
verifyTxType(txParams: TransactionParams, operations: stellar.Operation[]): void {
1093+
operations.forEach((operation) => {
1094+
if (!operation.type) throw new Error('Missing operation type on token enablement');
1095+
if (operation.type !== 'changeTrust')
1096+
throw new Error(`Invalid operation on token enablement: expected changeTrust, got ${operation.type}`);
1097+
});
1098+
}
1099+
1100+
verifyAssetType(txParams: TransactionParams, operations: stellar.Operation[]): void {
1101+
operations.forEach((operation) => {
1102+
const line = this.getTrustlineOperationLineOrThrow(operation);
1103+
const assetType = line.getAssetType();
1104+
if (assetType === 'liquidity_pool_shares')
1105+
throw new Error(`Invalid asset type on token enablement: got ${assetType}`);
1106+
});
1107+
}
1108+
1109+
verifyTokenIssuer(txParams: TransactionParams, operations: stellar.Operation[]): void {
1110+
const fullTokenData = this.getTokenDataOrThrow(txParams);
1111+
const expectedIssuer = this.getIssuerFromTokenName(fullTokenData);
1112+
1113+
operations.forEach((operation) => {
1114+
const line = this.getTrustlineOperationLineOrThrow(operation);
1115+
if (!('issuer' in line)) throw new Error('Missing issuer on token enablement operation');
1116+
if (line.issuer !== expectedIssuer)
1117+
throw new Error(`Invalid issuer on token enablement operation: expected ${expectedIssuer}, got ${line.issuer}`);
1118+
});
1119+
}
1120+
1121+
verifyTokenName(txParams: TransactionParams, operations: stellar.Operation[]): void {
1122+
const fullTokenData = this.getTokenDataOrThrow(txParams);
1123+
const expectedTokenCode = this.getTokenCodeFromTokenName(fullTokenData);
1124+
1125+
operations.forEach((operation) => {
1126+
const line = this.getTrustlineOperationLineOrThrow(operation);
1127+
if (!('code' in line)) throw new Error('Missing token code on token enablement operation');
1128+
if (line.code === '') throw new Error('Empty token code on token enablement operation');
1129+
if (line.code !== expectedTokenCode)
1130+
throw new Error(
1131+
`Invalid token code on token enablement operation: expected ${expectedTokenCode}, got ${line.code}`
1132+
);
1133+
});
1134+
}
1135+
1136+
verifyTokenLimits(txParams: TransactionParams, operations: stellar.Operation[]): void {
1137+
const recipient = this.getRecipientOrThrow(txParams);
1138+
1139+
operations.forEach((operation) => {
1140+
// trustline params use limits in base units
1141+
const line = this.getTrustlineOperationLineOrThrow(operation);
1142+
const limit = this.getTrustlineOperationLimitOrThrow(operation); // line should be stellar.Asset
1143+
if (!this.isOperationLineOfAssetType(line)) throw new Error('Invalid asset type');
1144+
const operationLimitBaseUnits = this.bigUnitsToBaseUnits(limit);
1145+
const operationToken = this.getTokenNameFromStellarAsset(line);
1146+
1147+
// Enable token limit is set to Xlm.maxTrustlineLimit by default
1148+
if (recipient.tokenName !== operationToken || operationLimitBaseUnits !== Xlm.maxTrustlineLimit) {
1149+
throw new Error('Token limit must be set to max limit on enable token operations');
1150+
}
1151+
});
1152+
}
1153+
10701154
/**
10711155
* Verify that a transaction prebuild complies with the original intention
10721156
*
@@ -1099,8 +1183,14 @@ export class Xlm extends BaseCoin {
10991183
(operation) => operation.type === 'createAccount' || operation.type === 'payment'
11001184
);
11011185

1102-
if (txParams.type === 'enabletoken') {
1103-
this.verifyEnableTokenTxOperations(tx.operations, txParams);
1186+
if (txParams.type === 'enabletoken' && verification.verifyTokenEnablement) {
1187+
const trustlineOperations = this.getTrustlineOperationsOrThrow(tx.operations, txParams, 'recipients');
1188+
this.verifyTxType(txParams, trustlineOperations);
1189+
1190+
this.verifyAssetType(txParams, trustlineOperations);
1191+
this.verifyTokenIssuer(txParams, trustlineOperations);
1192+
this.verifyTokenName(txParams, trustlineOperations);
1193+
this.verifyTokenLimits(txParams, trustlineOperations);
11041194
} else if (txParams.type === 'trustline') {
11051195
this.verifyTrustlineTxOperations(tx.operations, txParams);
11061196
} else {

0 commit comments

Comments
 (0)