Skip to content

Commit ef19b16

Browse files
Merge pull request #7006 from BitGo/WP-5819_xlm_token_enablements
feat(sdk-coin-xlm): blind signing guards for token enablements
2 parents c4fdfac + 8ed6ddd commit ef19b16

File tree

3 files changed

+931
-57
lines changed

3 files changed

+931
-57
lines changed

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

Lines changed: 145 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,37 @@ 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+
isChangeTrustOperation(operation: stellar.Operation): operation is stellar.Operation.ChangeTrust {
1013+
return operation.type && operation.type === 'changeTrust';
1014+
}
1015+
1016+
getTrustlineOperationLineOrThrow(operation: stellar.Operation): stellar.Asset | stellar.LiquidityPoolAsset {
1017+
if (this.isChangeTrustOperation(operation) && operation.line) return operation.line;
1018+
throw new Error('Invalid operation - expected changeTrust operation with line property');
1019+
}
1020+
1021+
getTrustlineOperationLimitOrThrow(operation: stellar.Operation): string {
1022+
if (this.isChangeTrustOperation(operation) && operation.limit) return operation.limit;
1023+
throw new Error('Invalid operation - expected changeTrust operation with limit property');
1024+
}
1025+
1026+
isOperationLineOfAssetType(line: stellar.Asset | stellar.LiquidityPoolAsset): line is stellar.Asset {
1027+
// line should be stellar.Asset, we removed the explicit cast and check the type instead
1028+
if (!line.getAssetType) return false;
1029+
return line.getAssetType() !== 'liquidity_pool_shares';
10281030
}
10291031

10301032
/**
@@ -1033,10 +1035,7 @@ export class Xlm extends BaseCoin {
10331035
* @param {TransactionParams} txParams - params used to build the tx
10341036
*/
10351037
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-
}
1038+
const trustlineOperations = this.getTrustlineOperationsOrThrow(operations, txParams, 'trustlines');
10401039
_.forEach(trustlineOperations, (op: stellar.Operation) => {
10411040
if (op.type !== 'changeTrust') {
10421041
throw new Error('Invalid asset type');
@@ -1067,6 +1066,98 @@ export class Xlm extends BaseCoin {
10671066
});
10681067
}
10691068

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

1102-
if (txParams.type === 'enabletoken') {
1103-
this.verifyEnableTokenTxOperations(tx.operations, txParams);
1193+
if (txParams.type === 'enabletoken' && verification.verifyTokenEnablement) {
1194+
const trustlineOperations = this.getTrustlineOperationsOrThrow(tx.operations, txParams, 'recipients');
1195+
this.verifyTxType(trustlineOperations);
1196+
this.verifyAssetType(txParams, trustlineOperations);
1197+
this.verifyTokenIssuer(txParams, trustlineOperations);
1198+
this.verifyTokenName(txParams, trustlineOperations);
1199+
this.verifyTokenLimits(txParams, trustlineOperations);
11041200
} else if (txParams.type === 'trustline') {
11051201
this.verifyTrustlineTxOperations(tx.operations, txParams);
11061202
} else {

0 commit comments

Comments
 (0)