Skip to content

Commit 2099f83

Browse files
feat(sdk-coin-sol): blind signing guards for token enablements
TICKET: WP-5744
1 parent dbb771b commit 2099f83

File tree

4 files changed

+569
-17
lines changed

4 files changed

+569
-17
lines changed

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

Lines changed: 109 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,92 @@ export class Sol extends BaseCoin {
233237
return Math.pow(10, this._staticsCoin.decimalPlaces);
234238
}
235239

240+
verifyTxType(txParams: TransactionParams, actualTypeFromDecoded: string | undefined): void {
241+
// do nothing, let the tx fail way down as always
242+
const expectedTypeFromUserParams = txParams.type;
243+
if (expectedTypeFromUserParams === undefined || actualTypeFromDecoded === undefined) return;
244+
245+
const correctPrebuildTxType = BLIND_SIGNING_TX_TYPES_TO_CHECK[expectedTypeFromUserParams];
246+
247+
if (correctPrebuildTxType && correctPrebuildTxType !== actualTypeFromDecoded) {
248+
throw new Error(
249+
`Tx type "${actualTypeFromDecoded}" does not match expected txParams type "${expectedTypeFromUserParams}"`
250+
);
251+
}
252+
}
253+
254+
throwIfMissingTokenEnablementsOrReturn(explanation: TransactionExplanation): ITokenEnablement[] {
255+
if (!explanation.tokenEnablements || explanation.tokenEnablements.length === 0)
256+
throw new Error('Missing tx token enablements data on token enablement tx prebuild');
257+
return explanation.tokenEnablements;
258+
}
259+
260+
throwIfMissingEnableTokenConfigOrReturn(txParams: TransactionParams): TokenEnablement {
261+
if (!txParams.enableTokens || txParams.enableTokens.length === 0) throw new Error('Missing enable token config');
262+
if (txParams.enableTokens.length > 1)
263+
throw new Error('Multiple token enablement not supported in a single transaction');
264+
return txParams.enableTokens[0];
265+
}
266+
267+
verifyTokenName(tokenEnablementsPrebuild: ITokenEnablement[], enableTokensConfig: TokenEnablement): void {
268+
const expectedTokenName = enableTokensConfig.name;
269+
tokenEnablementsPrebuild.forEach((tokenEnablement) => {
270+
if (!tokenEnablement.tokenName) throw new Error('Missing token name on token enablement tx');
271+
if (tokenEnablement.tokenName !== expectedTokenName)
272+
throw new Error(
273+
`Invalid token name: expected ${expectedTokenName}, got ${tokenEnablement.tokenName} on token enablement tx`
274+
);
275+
});
276+
}
277+
278+
async verifyTokenAddress(
279+
tokenEnablementsPrebuild: ITokenEnablement[],
280+
enableTokensConfig: TokenEnablement
281+
): Promise<void> {
282+
const expectedTokenAddress = enableTokensConfig.address;
283+
const expectedTokenName = enableTokensConfig.name;
284+
285+
if (!expectedTokenAddress) throw new Error('Missing token address on token enablement tx');
286+
if (!expectedTokenName) throw new Error('Missing token name on token enablement tx');
287+
288+
for (const tokenEnablement of tokenEnablementsPrebuild) {
289+
let tokenMintAddress: Readonly<SolCoin> | undefined;
290+
try {
291+
tokenMintAddress = getSolTokenFromTokenName(expectedTokenName);
292+
} catch {
293+
throw new Error(`Unable to derive ATA for token address: ${expectedTokenAddress}`);
294+
}
295+
if (
296+
!tokenMintAddress ||
297+
tokenMintAddress.tokenAddress === undefined ||
298+
tokenMintAddress.programId === undefined
299+
) {
300+
throw new Error(`Unable to get token mint address for ${expectedTokenName}`);
301+
}
302+
let ata: string;
303+
try {
304+
ata = await getAssociatedTokenAccountAddress(
305+
tokenMintAddress.tokenAddress,
306+
expectedTokenAddress,
307+
true,
308+
tokenMintAddress.programId
309+
);
310+
} catch {
311+
throw new Error(`Unable to derive ATA for token address: ${expectedTokenAddress}`);
312+
}
313+
if (ata !== tokenEnablement.address) {
314+
throw new Error(
315+
`Invalid token address: expected ${ata}, got ${tokenEnablement.address} on token enablement tx`
316+
);
317+
}
318+
}
319+
}
320+
321+
verifyNoOutputs(explanation: TransactionExplanation): void {
322+
if (!explanation.outputs) return;
323+
if (explanation.outputs.length > 0) throw new Error('Invalid token enablement tx: no outputs allow');
324+
}
325+
236326
async verifyTransaction(params: SolVerifyTransactionOptions): Promise<boolean> {
237327
// asset name to transfer amount map
238328
const totalAmount: Record<string, BigNumber> = {};
@@ -261,6 +351,16 @@ export class Sol extends BaseCoin {
261351
transaction.fromRawTransaction(rawTxBase64);
262352
const explainedTx = transaction.explainTransaction();
263353

354+
this.verifyTxType(txParams, explainedTx.type);
355+
if (txParams.type === 'enabletoken') {
356+
const tokenEnablementsPrebuild = this.throwIfMissingTokenEnablementsOrReturn(explainedTx);
357+
const enableTokensConfig = this.throwIfMissingEnableTokenConfigOrReturn(txParams);
358+
359+
this.verifyTokenName(tokenEnablementsPrebuild, enableTokensConfig);
360+
await this.verifyTokenAddress(tokenEnablementsPrebuild, enableTokensConfig);
361+
this.verifyNoOutputs(explainedTx);
362+
}
363+
264364
// users do not input recipients for consolidation requests as they are generated by the server
265365
if (txParams.recipients !== undefined) {
266366
const filteredRecipients = txParams.recipients?.map((recipient) =>

0 commit comments

Comments
 (0)