Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
905f41e
Permissioned Burn Extension
Szegoo Oct 31, 2025
f0175dc
compiles
Szegoo Oct 31, 2025
ea637bd
handle permissioned burn in processor
Szegoo Oct 31, 2025
d918f27
cleanup
Szegoo Oct 31, 2025
6a2e5bd
typo
Szegoo Oct 31, 2025
21b2e65
fmt
Szegoo Oct 31, 2025
e5ca5d4
remove unnecessary PermissionedBurnAccount
Szegoo Nov 1, 2025
d533794
Update interface/src/extension/mod.rs
Szegoo Nov 4, 2025
c22a5fe
add missing unpack & change index to 46
Szegoo Nov 4, 2025
f9133d3
implement authority type
Szegoo Nov 5, 2025
52931da
new PermissionedBurn instruction
Szegoo Nov 6, 2025
8a04278
small fix & add to rust-legacy
Szegoo Nov 8, 2025
05ee8e0
remove
Szegoo Nov 8, 2025
88b3d0b
move instruction under PermissionedBurnExtension
Szegoo Nov 26, 2025
32baa70
refactor
Szegoo Nov 26, 2025
c062644
rust-legacy test
Szegoo Nov 27, 2025
1cd3ce6
clean up & fix
Szegoo Nov 28, 2025
56e1a93
more cleanup
Szegoo Nov 28, 2025
d51e9dd
js-legacy test
Szegoo Nov 28, 2025
1f8cba9
add to cli
Szegoo Nov 28, 2025
4d7f325
add to js-client
Szegoo Nov 29, 2025
0aa93da
change order & separate enums
Szegoo Dec 5, 2025
7d9cea1
leftover ordering update & authority update test
Szegoo Dec 5, 2025
1adec9d
--permissioned-burn-authority
Szegoo Dec 5, 2025
ccc0238
handle None authority
Szegoo Dec 10, 2025
a6e910b
test None
Szegoo Dec 10, 2025
7970ee9
format & make clippy happy
Szegoo Dec 10, 2025
81b5af7
generate clients
Szegoo Dec 10, 2025
0df5cd2
Merge branch 'main' into permissioned-burn
Szegoo Dec 17, 2025
fa8911b
Update clients/cli/src/clap_app.rs
Szegoo Dec 17, 2025
789a206
fixes
Szegoo Dec 17, 2025
b36ebfc
fixes
Szegoo Dec 21, 2025
96414d2
fix nits
Szegoo Jan 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions clients/cli/src/clap_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,12 @@ pub fn app<'a>(
"Enable the mint authority to pause mint, burn, and transfer for this mint"
)
)
.arg(
Arg::with_name("enable_permissioned_burn")
.long("enable-permissioned-burn")
.takes_value(false)
.help("Require the configured permissioned burn authority for burning tokens")
)
.arg(multisig_signer_arg())
.nonce_args(true)
.arg(memo_arg())
Expand Down
6 changes: 6 additions & 0 deletions clients/cli/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ async fn command_create_token(
enable_transfer_hook: bool,
ui_multiplier: Option<f64>,
pausable: bool,
enable_permissioned_burn: bool,
bulk_signers: Vec<Arc<dyn Signer>>,
) -> CommandResult {
println_display(
Expand Down Expand Up @@ -409,6 +410,10 @@ async fn command_create_token(
extensions.push(ExtensionInitializationParams::PausableConfig { authority });
}

if enable_permissioned_burn {
extensions.push(ExtensionInitializationParams::PermissionedBurnConfig { authority });
}

let res = token
.create_mint(
&authority,
Expand Down Expand Up @@ -3804,6 +3809,7 @@ pub async fn process_command(
arg_matches.is_present("enable_transfer_hook"),
ui_multiplier,
arg_matches.is_present("enable_pause"),
arg_matches.is_present("enable_permissioned_burn"),
bulk_signers,
)
.await
Expand Down
33 changes: 33 additions & 0 deletions clients/cli/tests/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use {
metadata_pointer::MetadataPointer,
non_transferable::NonTransferable,
pausable::PausableConfig,
permissioned_burn::PermissionedBurnConfig,
scaled_ui_amount::ScaledUiAmountConfig,
transfer_fee::{TransferFeeAmount, TransferFeeConfig},
transfer_hook::TransferHook,
Expand Down Expand Up @@ -148,6 +149,7 @@ async fn main() {
async_trial!(compute_budget, test_validator, payer),
async_trial!(scaled_ui_amount, test_validator, payer),
async_trial!(pause, test_validator, payer),
async_trial!(permissioned_burn, test_validator, payer),
// GC messes with every other test, so have it on its own test validator
async_trial!(gc, gc_test_validator, gc_payer),
];
Expand Down Expand Up @@ -4507,3 +4509,34 @@ async fn pause(test_validator: &TestValidator, payer: &Keypair) {
let extension = test_mint.get_extension::<PausableConfig>().unwrap();
assert_eq!(Option::<Pubkey>::from(extension.authority), None,);
}

async fn permissioned_burn(test_validator: &TestValidator, payer: &Keypair) {
let config =
test_config_with_default_signer(test_validator, payer, &spl_token_2022_interface::id());

let token = Keypair::new();
let token_keypair_file = NamedTempFile::new().unwrap();
write_keypair_file(&token, &token_keypair_file).unwrap();
let token_pubkey = token.pubkey();

process_test_command(
&config,
payer,
&[
"spl-token",
CommandName::CreateToken.into(),
token_keypair_file.path().to_str().unwrap(),
"--enable-permissioned-burn",
],
)
.await
.unwrap();

let account = config.rpc_client.get_account(&token_pubkey).await.unwrap();
let test_mint = StateWithExtensionsOwned::<Mint>::unpack(account.data).unwrap();
let extension = test_mint.get_extension::<PermissionedBurnConfig>().unwrap();
assert_eq!(
Option::<Pubkey>::from(extension.authority),
Some(payer.pubkey())
);
}
7 changes: 7 additions & 0 deletions clients/js-legacy/src/extensions/extensionType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { MINT_CLOSE_AUTHORITY_SIZE } from './mintCloseAuthority.js';
import { NON_TRANSFERABLE_SIZE, NON_TRANSFERABLE_ACCOUNT_SIZE } from './nonTransferable.js';
import { PAUSABLE_CONFIG_SIZE, PAUSABLE_ACCOUNT_SIZE } from './pausable/index.js';
import { PERMANENT_DELEGATE_SIZE } from './permanentDelegate.js';
import { PERMISSIONED_BURN_SIZE } from './permissionedBurn/state.js';
import { SCALED_UI_AMOUNT_CONFIG_SIZE } from './scaledUiAmount/index.js';
import { TRANSFER_FEE_AMOUNT_SIZE, TRANSFER_FEE_CONFIG_SIZE } from './transferFee/index.js';
import { TRANSFER_HOOK_ACCOUNT_SIZE, TRANSFER_HOOK_SIZE } from './transferHook/index.js';
Expand Down Expand Up @@ -53,6 +54,7 @@ export enum ExtensionType {
ScaledUiAmountConfig = 25,
PausableConfig = 26,
PausableAccount = 27,
PermissionedBurn = 28,
}

export const TYPE_SIZE = 2;
Expand Down Expand Up @@ -123,6 +125,8 @@ export function getTypeLen(e: ExtensionType): number {
return PAUSABLE_CONFIG_SIZE;
case ExtensionType.PausableAccount:
return PAUSABLE_ACCOUNT_SIZE;
case ExtensionType.PermissionedBurn:
return PERMISSIONED_BURN_SIZE;
case ExtensionType.TokenMetadata:
throw Error(`Cannot get type length for variable extension type: ${e}`);
default:
Expand All @@ -148,6 +152,7 @@ export function isMintExtension(e: ExtensionType): boolean {
case ExtensionType.TokenGroupMember:
case ExtensionType.ScaledUiAmountConfig:
case ExtensionType.PausableConfig:
case ExtensionType.PermissionedBurn:
return true;
case ExtensionType.Uninitialized:
case ExtensionType.TransferFeeAmount:
Expand Down Expand Up @@ -192,6 +197,7 @@ export function isAccountExtension(e: ExtensionType): boolean {
case ExtensionType.TokenGroupMember:
case ExtensionType.ScaledUiAmountConfig:
case ExtensionType.PausableConfig:
case ExtensionType.PermissionedBurn:
return false;
default:
throw Error(`Unknown extension type: ${e}`);
Expand Down Expand Up @@ -230,6 +236,7 @@ export function getAccountTypeOfMintType(e: ExtensionType): ExtensionType {
case ExtensionType.TokenGroupMember:
case ExtensionType.ScaledUiAmountConfig:
case ExtensionType.PausableAccount:
case ExtensionType.PermissionedBurn:
return ExtensionType.Uninitialized;
}
}
Expand Down
1 change: 1 addition & 0 deletions clients/js-legacy/src/extensions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export * from './transferFee/index.js';
export * from './permanentDelegate.js';
export * from './transferHook/index.js';
export * from './pausable/index.js';
export * from './permissionedBurn/index.js';
2 changes: 2 additions & 0 deletions clients/js-legacy/src/extensions/permissionedBurn/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './instructions.js';
export * from './state.js';
182 changes: 182 additions & 0 deletions clients/js-legacy/src/extensions/permissionedBurn/instructions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { struct, u8 } from '@solana/buffer-layout';
import { publicKey, u64 } from '@solana/buffer-layout-utils';
import type { PublicKey, Signer } from '@solana/web3.js';
import { TransactionInstruction } from '@solana/web3.js';
import { programSupportsExtensions, TOKEN_2022_PROGRAM_ID } from '../../constants.js';
import { TokenUnsupportedInstructionError } from '../../errors.js';
import { addSigners } from '../../instructions/internal.js';
import { TokenInstruction } from '../../instructions/types.js';

export enum PermissionedBurnInstruction {
Initialize = 0,
Burn = 1,
BurnChecked = 2,
}

interface InitializePermissionedBurnInstructionData {
instruction: TokenInstruction.PermissionedBurnExtension;
permissionedBurnInstruction: PermissionedBurnInstruction.Initialize;
authority: PublicKey;
}

const initializePermissionedBurnInstructionData = struct<InitializePermissionedBurnInstructionData>([
u8('instruction'),
u8('permissionedBurnInstruction'),
publicKey('authority'),
]);

/**
* Construct a InitializePermissionedBurnConfig instruction
*
* @param mint Token mint account
* @param authority The permissioned burn mint's authority
* @param programId SPL Token program account
*/
export function createInitializePermissionedBurnInstruction(
mint: PublicKey,
authority: PublicKey,
programId = TOKEN_2022_PROGRAM_ID,
): TransactionInstruction {
if (!programSupportsExtensions(programId)) {
throw new TokenUnsupportedInstructionError();
}

const keys = [{ pubkey: mint, isSigner: false, isWritable: true }];
const data = Buffer.alloc(initializePermissionedBurnInstructionData.span);
initializePermissionedBurnInstructionData.encode(
{
instruction: TokenInstruction.PermissionedBurnExtension,
permissionedBurnInstruction: PermissionedBurnInstruction.Initialize,
authority,
},
data,
);

return new TransactionInstruction({ keys, programId, data });
}

interface PermissionedBurnInstructionData {
instruction: TokenInstruction.PermissionedBurnExtension;
permissionedBurnInstruction: PermissionedBurnInstruction.Burn;
amount: bigint;
}

const permissionedBurnInstructionData = struct<PermissionedBurnInstructionData>([
u8('instruction'),
u8('permissionedBurnInstruction'),
u64('amount'),
]);

/**
* Construct a permissioned burn instruction
*
* @param account Token account to update
* @param mint Token mint account
* @param owner The account's owner/delegate
* @param permissionedBurnAuthority The account's owner/delegate
* @param amount Amount to burn
* @param multiSigners The signer account(s)
* @param programId SPL Token program account
*/
export function createPermissionedBurnInstruction(
account: PublicKey,
mint: PublicKey,
owner: PublicKey,
permissionedBurnAuthority: PublicKey,
amount: number | bigint,
multiSigners: (Signer | PublicKey)[] = [],
programId = TOKEN_2022_PROGRAM_ID,
): TransactionInstruction {
if (!programSupportsExtensions(programId)) {
throw new TokenUnsupportedInstructionError();
}

const keys = addSigners(
[
{ pubkey: account, isSigner: false, isWritable: true },
{ pubkey: mint, isSigner: false, isWritable: true },
],
owner,
multiSigners,
);

// permissioned burn authority comes after the owner/delegate and before any multisig signers
keys.splice(3, 0, { pubkey: permissionedBurnAuthority, isSigner: true, isWritable: false });

const data = Buffer.alloc(permissionedBurnInstructionData.span);
permissionedBurnInstructionData.encode(
{
instruction: TokenInstruction.PermissionedBurnExtension,
permissionedBurnInstruction: PermissionedBurnInstruction.Burn,
amount: BigInt(amount),
},
data,
);

return new TransactionInstruction({ keys, programId, data });
}

interface PermissionedBurnCheckedInstructionData {
instruction: TokenInstruction.PermissionedBurnExtension;
permissionedBurnInstruction: PermissionedBurnInstruction.BurnChecked;
amount: bigint;
decimals: number;
}

const permissionedBurnCheckedInstructionData = struct<PermissionedBurnCheckedInstructionData>([
u8('instruction'),
u8('permissionedBurnInstruction'),
u64('amount'),
u8('decimals'),
]);

/**
* Construct a checked permissioned burn instruction
*
* @param account Token account to update
* @param mint Token mint account
* @param owner The account's owner/delegate
* @param permissionedBurnAuthority The account's owner/delegate
* @param amount Amount to burn
* @param decimals Number of the decimals of the mint
* @param multiSigners The signer account(s)
* @param programId SPL Token program account
*/
export function createPermissionedBurnCheckedInstruction(
account: PublicKey,
mint: PublicKey,
owner: PublicKey,
permissionedBurnAuthority: PublicKey,
amount: number | bigint,
decimals: number,
multiSigners: (Signer | PublicKey)[] = [],
programId = TOKEN_2022_PROGRAM_ID,
): TransactionInstruction {
if (!programSupportsExtensions(programId)) {
throw new TokenUnsupportedInstructionError();
}

const keys = addSigners(
[
{ pubkey: account, isSigner: false, isWritable: true },
{ pubkey: mint, isSigner: false, isWritable: true },
],
owner,
multiSigners,
);

keys.splice(3, 0, { pubkey: permissionedBurnAuthority, isSigner: true, isWritable: false });

const data = Buffer.alloc(permissionedBurnCheckedInstructionData.span);
permissionedBurnCheckedInstructionData.encode(
{
instruction: TokenInstruction.PermissionedBurnExtension,
permissionedBurnInstruction: PermissionedBurnInstruction.BurnChecked,
amount: BigInt(amount),
decimals,
},
data,
);

return new TransactionInstruction({ keys, programId, data });
}
27 changes: 27 additions & 0 deletions clients/js-legacy/src/extensions/permissionedBurn/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { struct } from '@solana/buffer-layout';
import { publicKey } from '@solana/buffer-layout-utils';
import { PublicKey } from '@solana/web3.js';
import type { Mint } from '../../state/mint.js';
import { ExtensionType, getExtensionData } from '../extensionType.js';

/** Permissioned burn configuration as stored by the program */
export interface PermissionedBurn {
authority: PublicKey | null;
}

/** Buffer layout for de/serializing a permissioned burn config */
export const PermissionedBurnLayout = struct<{ authority: PublicKey }>([publicKey('authority')]);

export const PERMISSIONED_BURN_SIZE = PermissionedBurnLayout.span;

export function getPermissionedBurn(mint: Mint): PermissionedBurn | null {
const extensionData = getExtensionData(ExtensionType.PermissionedBurn, mint.tlvData);
if (extensionData !== null) {
const { authority } = PermissionedBurnLayout.decode(extensionData);
return {
authority: authority.equals(PublicKey.default) ? null : authority,
};
} else {
return null;
}
}
1 change: 1 addition & 0 deletions clients/js-legacy/src/instructions/setAuthority.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export enum AuthorityType {
GroupMemberPointer = 14,
ScaledUiAmountConfig = 15,
PausableConfig = 16,
PermissionedBurn = 17,
}

/** TODO: docs */
Expand Down
1 change: 1 addition & 0 deletions clients/js-legacy/src/instructions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,5 @@ export enum TokenInstruction {
// ConfidentialMintBurnExtension = 42,
ScaledUiAmountExtension = 43,
PausableExtension = 44,
PermissionedBurnExtension = 46,
}
Loading
Loading