Skip to content

Commit a595ab9

Browse files
Merge pull request #6966 from BitGo/WP-5744_output_sol_token_enablements
feat(sdk-coin-sol): blind signing token enablements guards
2 parents ce0e0af + eb9ebe9 commit a595ab9

File tree

5 files changed

+705
-32
lines changed

5 files changed

+705
-32
lines changed

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

Lines changed: 100 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
* @prettier
33
*/
44

5+
import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token';
56
import BigNumber from 'bignumber.js';
67
import * as base58 from 'bs58';
8+
import * as _ from 'lodash';
9+
import * as request from 'superagent';
710

811
import {
12+
AuditDecryptedKeyParams,
913
BaseBroadcastTransactionOptions,
1014
BaseBroadcastTransactionResult,
1115
BaseCoin,
@@ -16,6 +20,7 @@ import {
1620
EDDSAMethods,
1721
EDDSAMethodTypes,
1822
Environments,
23+
ITokenEnablement,
1924
KeyPair,
2025
Memo,
2126
MethodNotImplementedError,
@@ -27,29 +32,28 @@ import {
2732
MPCTx,
2833
MPCTxs,
2934
MPCUnsignedTx,
35+
MultisigType,
36+
multisigTypes,
3037
OvcInput,
3138
OvcOutput,
3239
ParsedTransaction,
40+
PopulatedIntent,
41+
PrebuildTransactionWithIntentOptions,
3342
PresignTransactionOptions,
3443
PublicKey,
3544
RecoveryTxRequest,
3645
SignedTransaction,
3746
SignTransactionOptions,
47+
TokenEnablement,
3848
TokenEnablementConfig,
3949
TransactionExplanation,
50+
TransactionParams,
4051
TransactionRecipient,
4152
VerifyAddressOptions,
4253
VerifyTransactionOptions,
43-
MultisigType,
44-
multisigTypes,
45-
AuditDecryptedKeyParams,
46-
PopulatedIntent,
47-
PrebuildTransactionWithIntentOptions,
4854
} from '@bitgo/sdk-core';
4955
import { auditEddsaPrivateKey, getDerivationPath } from '@bitgo/sdk-lib-mpc';
50-
import { BaseNetwork, CoinFamily, coins, BaseCoin as StaticsBaseCoin } from '@bitgo/statics';
51-
import * as _ from 'lodash';
52-
import * as request from 'superagent';
56+
import { BaseNetwork, CoinFamily, coins, SolCoin, BaseCoin as StaticsBaseCoin } from '@bitgo/statics';
5357
import { KeyPair as SolKeyPair, Transaction, TransactionBuilder, TransactionBuilderFactory } from './lib';
5458
import {
5559
getAssociatedTokenAccountAddress,
@@ -60,7 +64,6 @@ import {
6064
isValidPublicKey,
6165
validateRawTransaction,
6266
} from './lib/utils';
63-
import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token';
6467

6568
export const DEFAULT_SCAN_FACTOR = 20; // default number of receive addresses to scan for funds
6669

@@ -173,6 +176,7 @@ export interface SolConsolidationRecoveryOptions extends MPCConsolidationRecover
173176
}
174177

175178
const HEX_REGEX = /^[0-9a-fA-F]+$/;
179+
const BLIND_SIGNING_TX_TYPES_TO_CHECK = { enabletoken: 'AssociatedTokenAccountInitialization' };
176180

177181
export class Sol extends BaseCoin {
178182
protected readonly _staticsCoin: Readonly<StaticsBaseCoin>;
@@ -233,6 +237,84 @@ export class Sol extends BaseCoin {
233237
return Math.pow(10, this._staticsCoin.decimalPlaces);
234238
}
235239

240+
verifyTxType(expectedTypeFromUserParams: string, actualTypeFromDecoded: string | undefined): void {
241+
const matchFromUserToDecodedType = BLIND_SIGNING_TX_TYPES_TO_CHECK[expectedTypeFromUserParams];
242+
if (matchFromUserToDecodedType !== actualTypeFromDecoded) {
243+
throw new Error(
244+
`Invalid transaction type on token enablement: expected "${matchFromUserToDecodedType}", got "${actualTypeFromDecoded}".`
245+
);
246+
}
247+
}
248+
249+
throwIfMissingTokenEnablementsOrReturn(explanation: TransactionExplanation): ITokenEnablement[] {
250+
if (!explanation.tokenEnablements || explanation.tokenEnablements.length === 0)
251+
throw new Error('Missing tx token enablements data on token enablement tx prebuild');
252+
return explanation.tokenEnablements;
253+
}
254+
255+
throwIfMissingEnableTokenConfigOrReturn(txParams: TransactionParams): TokenEnablement[] {
256+
if (!txParams.enableTokens || txParams.enableTokens.length === 0) throw new Error('Missing enable token config');
257+
return txParams.enableTokens;
258+
}
259+
260+
verifyTokenName(tokenEnablementsPrebuild: ITokenEnablement[], enableTokensConfig: TokenEnablement[]): void {
261+
enableTokensConfig.forEach((enableTokenConfig) => {
262+
const expectedTokenName = enableTokenConfig.name;
263+
tokenEnablementsPrebuild.forEach((tokenEnablement) => {
264+
if (!tokenEnablement.tokenName) throw new Error('Missing token name on token enablement tx');
265+
if (tokenEnablement.tokenName !== expectedTokenName)
266+
throw new Error(
267+
`Invalid token name: expected ${expectedTokenName}, got ${tokenEnablement.tokenName} on token enablement tx`
268+
);
269+
});
270+
});
271+
}
272+
273+
async verifyTokenAddress(
274+
tokenEnablementsPrebuild: ITokenEnablement[],
275+
enableTokensConfig: TokenEnablement[]
276+
): Promise<void> {
277+
for (const enableTokenConfig of enableTokensConfig) {
278+
const expectedTokenAddress = enableTokenConfig.address;
279+
const expectedTokenName = enableTokenConfig.name;
280+
281+
if (!expectedTokenAddress) throw new Error('Missing token address on token enablement tx');
282+
if (!expectedTokenName) throw new Error('Missing token name on token enablement tx');
283+
284+
for (const tokenEnablement of tokenEnablementsPrebuild) {
285+
let tokenMintAddress: Readonly<SolCoin> | undefined;
286+
try {
287+
tokenMintAddress = getSolTokenFromTokenName(expectedTokenName);
288+
} catch {
289+
throw new Error(`Unable to derive ATA for token address: ${expectedTokenAddress}`);
290+
}
291+
if (
292+
!tokenMintAddress ||
293+
tokenMintAddress.tokenAddress === undefined ||
294+
tokenMintAddress.programId === undefined
295+
) {
296+
throw new Error(`Unable to get token mint address for ${expectedTokenName}`);
297+
}
298+
let ata: string;
299+
try {
300+
ata = await getAssociatedTokenAccountAddress(
301+
tokenMintAddress.tokenAddress,
302+
expectedTokenAddress,
303+
true,
304+
tokenMintAddress.programId
305+
);
306+
} catch {
307+
throw new Error(`Unable to derive ATA for token address: ${expectedTokenAddress}`);
308+
}
309+
if (ata !== tokenEnablement.address) {
310+
throw new Error(
311+
`Invalid token address: expected ${ata}, got ${tokenEnablement.address} on token enablement tx`
312+
);
313+
}
314+
}
315+
}
316+
}
317+
236318
async verifyTransaction(params: SolVerifyTransactionOptions): Promise<boolean> {
237319
// asset name to transfer amount map
238320
const totalAmount: Record<string, BigNumber> = {};
@@ -261,6 +343,15 @@ export class Sol extends BaseCoin {
261343
transaction.fromRawTransaction(rawTxBase64);
262344
const explainedTx = transaction.explainTransaction();
263345

346+
if (txParams.type === 'enabletoken' && verificationOptions?.verifyTokenEnablement) {
347+
this.verifyTxType(txParams.type, explainedTx.type);
348+
const tokenEnablementsPrebuild = this.throwIfMissingTokenEnablementsOrReturn(explainedTx);
349+
const enableTokensConfig = this.throwIfMissingEnableTokenConfigOrReturn(txParams);
350+
351+
this.verifyTokenName(tokenEnablementsPrebuild, enableTokensConfig);
352+
await this.verifyTokenAddress(tokenEnablementsPrebuild, enableTokensConfig);
353+
}
354+
264355
// users do not input recipients for consolidation requests as they are generated by the server
265356
if (txParams.recipients !== undefined) {
266357
const filteredRecipients = txParams.recipients?.map((recipient) =>

0 commit comments

Comments
 (0)