Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 56b1a95

Browse files
authored
token-js: sanity check header space in tlv data (#3936)
1 parent 8dde48e commit 56b1a95

File tree

4 files changed

+160
-3
lines changed

4 files changed

+160
-3
lines changed

token/js/src/extensions/extensionType.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,50 @@ export function getTypeLen(e: ExtensionType): number {
6767
}
6868
}
6969

70+
export function isMintExtension(e: ExtensionType): boolean {
71+
switch (e) {
72+
case ExtensionType.TransferFeeConfig:
73+
case ExtensionType.MintCloseAuthority:
74+
case ExtensionType.ConfidentialTransferMint:
75+
case ExtensionType.DefaultAccountState:
76+
case ExtensionType.NonTransferable:
77+
case ExtensionType.InterestBearingConfig:
78+
case ExtensionType.PermanentDelegate:
79+
return true;
80+
case ExtensionType.Uninitialized:
81+
case ExtensionType.TransferFeeAmount:
82+
case ExtensionType.ConfidentialTransferAccount:
83+
case ExtensionType.ImmutableOwner:
84+
case ExtensionType.MemoTransfer:
85+
case ExtensionType.CpiGuard:
86+
return false;
87+
default:
88+
throw Error(`Unknown extension type: ${e}`);
89+
}
90+
}
91+
92+
export function isAccountExtension(e: ExtensionType): boolean {
93+
switch (e) {
94+
case ExtensionType.TransferFeeAmount:
95+
case ExtensionType.ConfidentialTransferAccount:
96+
case ExtensionType.ImmutableOwner:
97+
case ExtensionType.MemoTransfer:
98+
case ExtensionType.CpiGuard:
99+
return true;
100+
case ExtensionType.Uninitialized:
101+
case ExtensionType.TransferFeeConfig:
102+
case ExtensionType.MintCloseAuthority:
103+
case ExtensionType.ConfidentialTransferMint:
104+
case ExtensionType.DefaultAccountState:
105+
case ExtensionType.NonTransferable:
106+
case ExtensionType.InterestBearingConfig:
107+
case ExtensionType.PermanentDelegate:
108+
return false;
109+
default:
110+
throw Error(`Unknown extension type: ${e}`);
111+
}
112+
}
113+
70114
export function getAccountTypeOfMintType(e: ExtensionType): ExtensionType {
71115
switch (e) {
72116
case ExtensionType.TransferFeeConfig:
@@ -117,7 +161,7 @@ export function getAccountLen(extensionTypes: ExtensionType[]): number {
117161

118162
export function getExtensionData(extension: ExtensionType, tlvData: Buffer): Buffer | null {
119163
let extensionTypeIndex = 0;
120-
while (extensionTypeIndex < tlvData.length) {
164+
while (extensionTypeIndex + TYPE_SIZE + LENGTH_SIZE <= tlvData.length) {
121165
const entryType = tlvData.readUInt16LE(extensionTypeIndex);
122166
const entryLength = tlvData.readUInt16LE(extensionTypeIndex + TYPE_SIZE);
123167
const typeIndex = extensionTypeIndex + TYPE_SIZE + LENGTH_SIZE;

token/js/test/e2e-2022/cpiGuard.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,17 @@ describe('cpiGuard', () => {
6969

7070
it('enable/disable via instruction', async () => {
7171
let accountInfo = await getAccount(connection, account, undefined, TEST_PROGRAM_ID);
72+
let cpiGuard = getCpiGuard(accountInfo);
73+
74+
expect(cpiGuard).to.be.null;
7275

7376
let transaction = new Transaction().add(
7477
createEnableCpiGuardInstruction(account, owner.publicKey, [], TEST_PROGRAM_ID)
7578
);
7679
await sendAndConfirmTransaction(connection, transaction, [payer, owner], undefined);
7780

7881
accountInfo = await getAccount(connection, account, undefined, TEST_PROGRAM_ID);
79-
let cpiGuard = getCpiGuard(accountInfo);
82+
cpiGuard = getCpiGuard(accountInfo);
8083

8184
expect(cpiGuard).to.not.be.null;
8285
if (cpiGuard !== null) {
@@ -99,11 +102,14 @@ describe('cpiGuard', () => {
99102

100103
it('enable/disable via command', async () => {
101104
let accountInfo = await getAccount(connection, account, undefined, TEST_PROGRAM_ID);
105+
let cpiGuard = getCpiGuard(accountInfo);
106+
107+
expect(cpiGuard).to.be.null;
102108

103109
await enableCpiGuard(connection, payer, account, owner, [], undefined, TEST_PROGRAM_ID);
104110

105111
accountInfo = await getAccount(connection, account, undefined, TEST_PROGRAM_ID);
106-
let cpiGuard = getCpiGuard(accountInfo);
112+
cpiGuard = getCpiGuard(accountInfo);
107113

108114
expect(cpiGuard).to.not.be.null;
109115
if (cpiGuard !== null) {

token/js/test/e2e-2022/tlv.test.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import chai, { expect } from 'chai';
2+
import chaiAsPromised from 'chai-as-promised';
3+
chai.use(chaiAsPromised);
4+
5+
import type { Connection, PublicKey, Signer } from '@solana/web3.js';
6+
import { sendAndConfirmTransaction, Keypair, SystemProgram, Transaction } from '@solana/web3.js';
7+
8+
import type { Account, Mint } from '../../src';
9+
import {
10+
createInitializeAccountInstruction,
11+
getAccount,
12+
getAccountLen,
13+
createMint,
14+
ExtensionType,
15+
getExtensionData,
16+
isAccountExtension,
17+
} from '../../src';
18+
import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common';
19+
20+
const TEST_TOKEN_DECIMALS = 9;
21+
const ACCOUNT_EXTENSIONS = Object.values(ExtensionType)
22+
.filter(Number.isInteger)
23+
.filter((e: any): e is ExtensionType => isAccountExtension(e));
24+
25+
describe('tlv test', () => {
26+
let connection: Connection;
27+
let payer: Signer;
28+
let owner: Keypair;
29+
30+
before(async () => {
31+
connection = await getConnection();
32+
payer = await newAccountWithLamports(connection, 1000000000);
33+
owner = Keypair.generate();
34+
});
35+
36+
// test that the parser gracefully handles accounts with arbitrary extra bytes
37+
it('parse account with extra bytes', async () => {
38+
const initTestAccount = async (extraBytes: number) => {
39+
const mintKeypair = Keypair.generate();
40+
const accountKeypair = Keypair.generate();
41+
const account = accountKeypair.publicKey;
42+
const accountLen = getAccountLen([]) + extraBytes;
43+
const lamports = await connection.getMinimumBalanceForRentExemption(accountLen);
44+
45+
const mint = await createMint(
46+
connection,
47+
payer,
48+
mintKeypair.publicKey,
49+
mintKeypair.publicKey,
50+
TEST_TOKEN_DECIMALS,
51+
mintKeypair,
52+
undefined,
53+
TEST_PROGRAM_ID
54+
);
55+
56+
const transaction = new Transaction().add(
57+
SystemProgram.createAccount({
58+
fromPubkey: payer.publicKey,
59+
newAccountPubkey: account,
60+
space: accountLen,
61+
lamports,
62+
programId: TEST_PROGRAM_ID,
63+
}),
64+
createInitializeAccountInstruction(account, mint, owner.publicKey, TEST_PROGRAM_ID)
65+
);
66+
67+
await sendAndConfirmTransaction(connection, transaction, [payer, accountKeypair], undefined);
68+
69+
return account;
70+
};
71+
72+
const promises: Promise<[number, Account] | undefined>[] = [];
73+
for (let i = 0; i < 16; i++) {
74+
// trying to alloc exactly one extra byte causes an unpack failure in the program when initializing
75+
if (i == 1) continue;
76+
77+
promises.push(
78+
initTestAccount(i)
79+
.then((account: PublicKey) => getAccount(connection, account, undefined, TEST_PROGRAM_ID))
80+
.then((accountInfo: Account) => {
81+
for (const extension of ACCOUNT_EXTENSIONS) {
82+
// realistically this will never fail with a non-null value, it will just throw
83+
expect(
84+
getExtensionData(extension, accountInfo.tlvData),
85+
`account parse test failed: found ${ExtensionType[extension]}, but should not have. \
86+
test case: no extensions, ${i} extra bytes`
87+
).to.be.null;
88+
}
89+
return Promise.resolve(undefined);
90+
})
91+
);
92+
}
93+
94+
await Promise.all(promises);
95+
});
96+
});

token/js/test/unit/index.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {
1616
TokenOwnerOffCurveError,
1717
getAccountLen,
1818
ExtensionType,
19+
isMintExtension,
20+
isAccountExtension,
1921
getAssociatedTokenAddressSync,
2022
createInitializeAccount2Instruction,
2123
createInitializeAccount3Instruction,
@@ -238,4 +240,13 @@ describe('extensionType', () => {
238240
expect(getAccountLen([ExtensionType.ImmutableOwner])).to.eql(170);
239241
expect(getAccountLen([ExtensionType.PermanentDelegate])).to.eql(202);
240242
});
243+
244+
it('exclusive and exhaustive predicates', () => {
245+
const exts = Object.values(ExtensionType).filter(Number.isInteger);
246+
const mintExts = exts.filter((e: any): e is ExtensionType => isMintExtension(e));
247+
const accountExts = exts.filter((e: any): e is ExtensionType => isAccountExtension(e));
248+
const collectedExts = [ExtensionType.Uninitialized].concat(mintExts, accountExts);
249+
250+
expect(collectedExts.sort()).to.eql(exts.sort());
251+
});
241252
});

0 commit comments

Comments
 (0)