授权销毁

什么是授权销毁?

Token Extensions Program 的 PermissionedBurnConfig 铸币扩展要求已配置的销毁授权方对该铸币的每次销毁进行共同签名。

在销毁授权方设置期间:

  • 标准的 BurnBurnChecked 指令将失败,并返回 TokenError::InvalidInstruction
  • 销毁操作必须使用 PermissionedBurnInstruction::BurnPermissionedBurnInstruction::BurnChecked,并由销毁授权方与 token account 的所有者或委托方共同签名。

销毁授权方是共同签名者,而非所有者的替代角色。授权方单独无法从他人的 token account 中销毁代币,token account 的所有者也无法在没有授权方签名的情况下进行销毁。永久委托方同样必须使用授权销毁指令,并仍需销毁授权方的共同签名。

这使发行方能够将代币供应量与链下记录保持同步,例如必须保持 1:1 支撑的代币化资产,从而防止持有者单方面销毁代币。

销毁授权方可以通过 SetAuthority 使用 AuthorityType::PermissionedBurn 进行轮换。将授权方设置为 None 将禁用授权销毁并重新启用标准销毁指令。扩展数据本身仍保留在铸币账户上。

本地测试 validator 还内置了较旧版本的 Token-2022,因此若要运行本页面的示例,请将 devnet 上的 v11.0.0 程序加载到您的测试 validator 中:

Terminal
solana program dump -u devnet TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb token_2022.so
solana-test-validator --reset --bpf-program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb token_2022.so

如何创建带权限的铸造与销毁

要创建带权限销毁的 mint account 并销毁代币:

  1. 计算 mint account 所需的大小以及 PermissionedBurnConfig 扩展所需的 rent。
  2. 使用 CreateAccount 创建 mint account,初始化 PermissionedBurnConfig,并使用 InitializeMint 初始化该 mint account。
  3. 创建 token account 并铸造代币。
  4. 使用 PermissionedBurnInstruction::BurnChecked 进行销毁,需由 token account 所有者和销毁授权方共同签名。
  5. 可选操作:使用 AuthorityType::PermissionedBurnNone 授权方,通过 SetAuthority 禁用权限销毁,从而重新启用标准销毁指令。以下完整代码示例展示了此步骤。

计算账户大小

计算基础 mint account 加上 PermissionedBurnConfig 扩展所需的 mint account 大小。该大小将用于 CreateAccount

Example
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const mint = await generateKeyPairSigner();
const burnAuthority = await generateKeyPairSigner();
const permissionedBurnExtension = extension("PermissionedBurn", {
authority: burnAuthority.address
});
const mintSpace = BigInt(getMintSize([permissionedBurnExtension]));

计算 rent

使用 mint account 加上 PermissionedBurnConfig 扩展所需的大小来计算 rent。

Example
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const mint = await generateKeyPairSigner();
const burnAuthority = await generateKeyPairSigner();
const permissionedBurnExtension = extension("PermissionedBurn", {
authority: burnAuthority.address
});
const mintSpace = BigInt(getMintSize([permissionedBurnExtension]));
const mintRent = await client.rpc
.getMinimumBalanceForRentExemption(mintSpace)
.send();

创建 mint account

使用计算得出的空间和 lamport 创建 mint account。

Example
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const mint = await generateKeyPairSigner();
const burnAuthority = await generateKeyPairSigner();
const permissionedBurnExtension = extension("PermissionedBurn", {
authority: burnAuthority.address
});
const mintSpace = BigInt(getMintSize([permissionedBurnExtension]));
const mintRent = await client.rpc
.getMinimumBalanceForRentExemption(mintSpace)
.send();
await client.sendTransaction([
getCreateAccountInstruction({
payer: client.payer,
newAccount: mint,
lamports: mintRent,
space: mintSpace,
programAddress: TOKEN_2022_PROGRAM_ADDRESS
})
]);

初始化 PermissionedBurn

在 mint account 上初始化 PermissionedBurnConfig 扩展,并设置销毁授权方。

Example
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const mint = await generateKeyPairSigner();
const burnAuthority = await generateKeyPairSigner();
const permissionedBurnExtension = extension("PermissionedBurn", {
authority: burnAuthority.address
});
const mintSpace = BigInt(getMintSize([permissionedBurnExtension]));
const mintRent = await client.rpc
.getMinimumBalanceForRentExemption(mintSpace)
.send();
await client.sendTransaction([
getCreateAccountInstruction({
payer: client.payer,
newAccount: mint,
lamports: mintRent,
space: mintSpace,
programAddress: TOKEN_2022_PROGRAM_ADDRESS
}),
getInitializePermissionedBurnInstruction({
mint: mint.address,
authority: burnAuthority.address
})
]);

初始化 mint account

在同一交易中使用 InitializeMint 初始化 mint account。

Example
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const mint = await generateKeyPairSigner();
const burnAuthority = await generateKeyPairSigner();
const permissionedBurnExtension = extension("PermissionedBurn", {
authority: burnAuthority.address
});
const mintSpace = BigInt(getMintSize([permissionedBurnExtension]));
const mintRent = await client.rpc
.getMinimumBalanceForRentExemption(mintSpace)
.send();
await client.sendTransaction([
getCreateAccountInstruction({
payer: client.payer,
newAccount: mint,
lamports: mintRent,
space: mintSpace,
programAddress: TOKEN_2022_PROGRAM_ADDRESS
}),
getInitializePermissionedBurnInstruction({
mint: mint.address,
authority: burnAuthority.address
}),
getInitializeMintInstruction({
mint: mint.address,
decimals: 0,
mintAuthority: client.payer.address,
freezeAuthority: client.payer.address
})
]);

创建 token account 并铸造代币

为付款方创建一个 token account 并向其铸造代币。

使用销毁权限进行销毁

使用由 token account 所有者和销毁权限方共同签名的 PermissionedBurnInstruction::BurnChecked 来销毁代币。

计算账户大小

计算基础 mint account 加上 PermissionedBurnConfig 扩展所需的 mint account 大小。该大小将用于 CreateAccount

计算 rent

使用 mint account 加上 PermissionedBurnConfig 扩展所需的大小来计算 rent。

创建 mint account

使用计算得出的空间和 lamport 创建 mint account。

初始化 PermissionedBurn

在 mint account 上初始化 PermissionedBurnConfig 扩展,并设置销毁授权方。

初始化 mint account

在同一交易中使用 InitializeMint 初始化 mint account。

创建 token account 并铸造代币

为付款方创建一个 token account 并向其铸造代币。

使用销毁权限进行销毁

使用由 token account 所有者和销毁权限方共同签名的 PermissionedBurnInstruction::BurnChecked 来销毁代币。

Example
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const mint = await generateKeyPairSigner();
const burnAuthority = await generateKeyPairSigner();
const permissionedBurnExtension = extension("PermissionedBurn", {
authority: burnAuthority.address
});
const mintSpace = BigInt(getMintSize([permissionedBurnExtension]));

指令顺序

PermissionedBurnInstruction::Initialize 必须在 InitializeMint 之前执行。CreateAccountPermissionedBurnInstruction::InitializeInitializeMint 必须 包含在同一笔交易中。

源码参考

条目描述来源
PermissionedBurnConfigMint 扩展,用于存储对该 mint 每次销毁操作进行联署所需的权限。来源
PermissionedBurnInstruction::InitializeInitializeMint 之前初始化受权限控制的销毁配置的指令。来源
PermissionedBurnInstruction::Burn需要销毁权限方以及 token account 所有者或委托方签名的销毁指令。来源
PermissionedBurnInstruction::BurnChecked带有小数位检查的销毁指令,使用与 PermissionedBurnInstruction::Burn 相同的账户。来源
AuthorityType::PermissionedBurnSetAuthority 配合使用的权限鉴别器,用于轮换或禁用 mint 上的销毁权限。来源
process_initialize处理器逻辑,用于在未初始化的 mint 上初始化 PermissionedBurnConfig 并存储销毁权限。来源
process_burn处理器逻辑,在设置了销毁权限时拒绝标准销毁操作并验证权限方的签名。来源
process_set_authority处理器逻辑,在使用 AuthorityType::PermissionedBurn 时验证并轮换销毁权限。来源

Typescript

以下 Kit 示例直接使用生成的指令。已发布的旧版 @solana/spl-token 包尚不支持权限销毁功能,因此不包含旧版示例。

Kit

Instructions
import {
lamports,
createClient,
generateKeyPairSigner,
unwrapOption
} from "@solana/kit";
import { solanaRpc, rpcAirdrop } from "@solana/kit-plugin-rpc";
import { generatedPayer, airdropPayer } from "@solana/kit-plugin-signer";
import { getCreateAccountInstruction } from "@solana-program/system";
import {
AuthorityType,
extension,
fetchMint,
fetchToken,
findAssociatedTokenPda,
getBurnCheckedInstruction,
getCreateAssociatedTokenInstructionAsync,
getInitializeMintInstruction,
getInitializePermissionedBurnInstruction,
getMintSize,
getMintToCheckedInstruction,
getPermissionedBurnCheckedInstruction,
getSetAuthorityInstruction,
isExtension,
TOKEN_2022_PROGRAM_ADDRESS
} from "@solana-program/token-2022";
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const mint = await generateKeyPairSigner();
const burnAuthority = await generateKeyPairSigner();
const permissionedBurnExtension = extension("PermissionedBurn", {
authority: burnAuthority.address
});
const mintSpace = BigInt(getMintSize([permissionedBurnExtension]));
const mintRent = await client.rpc
.getMinimumBalanceForRentExemption(mintSpace)
.send();
await client.sendTransaction([
getCreateAccountInstruction({
payer: client.payer, // Account funding account creation.
newAccount: mint, // New mint account to create.
lamports: mintRent, // Lamports funding the mint account rent.
space: mintSpace, // Account size in bytes for the mint plus PermissionedBurnConfig.
programAddress: TOKEN_2022_PROGRAM_ADDRESS // Program that owns the mint account.
}),
getInitializePermissionedBurnInstruction({
mint: mint.address, // Mint account that stores the PermissionedBurnConfig extension.
authority: burnAuthority.address // Authority required to co-sign every burn for the mint.
}),
getInitializeMintInstruction({
mint: mint.address, // Mint account to initialize.
decimals: 0, // Number of decimals for the token.
mintAuthority: client.payer.address, // Authority allowed to mint new tokens.
freezeAuthority: client.payer.address // Authority allowed to freeze token accounts.
})
]);
const [sourceToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: client.payer.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
await client.sendTransaction([
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer, // Account funding the associated token account creation.
mint: mint.address, // Mint for the associated token account.
owner: client.payer.address // Owner of the associated token account.
}),
getMintToCheckedInstruction({
mint: mint.address, // Mint account that issues the tokens.
token: sourceToken, // Token account receiving the newly minted tokens.
mintAuthority: client.payer, // Signer authorized to mint new tokens.
amount: 2n, // Token amount in base units.
decimals: 0 // Decimals defined on the mint.
})
]);
let standardBurnFailure: string | undefined;
try {
await client.sendTransaction([
getBurnCheckedInstruction({
account: sourceToken, // Token account to burn from.
mint: mint.address, // Mint with the permissioned burn configuration.
authority: client.payer, // Token account owner signing the burn.
amount: 1n, // Token amount in base units.
decimals: 0 // Decimals defined on the mint.
})
]);
} catch (error) {
standardBurnFailure = error instanceof Error ? error.message : String(error);
}
if (!standardBurnFailure) {
throw new Error("Expected the standard burn to fail");
}
await client.sendTransaction([
getPermissionedBurnCheckedInstruction({
account: sourceToken, // Token account to burn from.
mint: mint.address, // Mint with the permissioned burn configuration.
permissionedBurnAuthority: burnAuthority, // Burn authority co-signing the burn.
authority: client.payer, // Token account owner signing the burn.
amount: 1n, // Token amount in base units.
decimals: 0 // Decimals defined on the mint.
})
]);
await client.sendTransaction([
getSetAuthorityInstruction({
owned: mint.address, // Mint with the permissioned burn configuration.
owner: burnAuthority, // Current burn authority signing the update.
authorityType: AuthorityType.PermissionedBurn, // Authority type to update.
newAuthority: null // Setting the authority to None disables permissioned burning.
})
]);
await client.sendTransaction([
getBurnCheckedInstruction({
account: sourceToken, // Token account to burn from.
mint: mint.address, // Mint with permissioned burning disabled.
authority: client.payer, // Token account owner signing the burn.
amount: 1n, // Token amount in base units.
decimals: 0 // Decimals defined on the mint.
})
]);
const sourceAccount = await fetchToken(client.rpc, sourceToken);
const mintAccount = await fetchMint(client.rpc, mint.address);
const permissionedBurnConfig = (
unwrapOption(mintAccount.data.extensions) ?? []
).find((item) => isExtension("PermissionedBurn", item));
console.log("Mint Address:", mint.address);
console.log("Error From Failed Standard Burn:", standardBurnFailure);
console.log("Source Amount After Burns:", sourceAccount.data.amount);
console.log("Extension After Disable:", permissionedBurnConfig);

Rust

Rust
use anyhow::{anyhow, Result};
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_commitment_config::CommitmentConfig;
use solana_sdk::{
signature::{Keypair, Signer},
transaction::Transaction,
};
use solana_system_interface::instruction::create_account;
use spl_associated_token_account_interface::{
address::get_associated_token_address_with_program_id,
instruction::create_associated_token_account,
};
use spl_token_2022_interface::{
extension::{
permissioned_burn::{instruction as permissioned_burn_ix, PermissionedBurnConfig},
BaseStateWithExtensions, ExtensionType, StateWithExtensions,
},
instruction::{burn_checked, initialize_mint, mint_to_checked, set_authority, AuthorityType},
state::{Account, Mint},
ID as TOKEN_2022_PROGRAM_ID,
};
#[tokio::main]
async fn main() -> Result<()> {
let client = RpcClient::new_with_commitment(
String::from("http://localhost:8899"),
CommitmentConfig::confirmed(),
);
let fee_payer = Keypair::new();
let burn_authority = Keypair::new();
let airdrop_signature = client
.request_airdrop(&fee_payer.pubkey(), 5_000_000_000)
.await?;
loop {
let confirmed = client.confirm_transaction(&airdrop_signature).await?;
if confirmed {
break;
}
}
let mint = Keypair::new();
let mint_space =
ExtensionType::try_calculate_account_len::<Mint>(&[ExtensionType::PermissionedBurn])?;
let mint_rent = client
.get_minimum_balance_for_rent_exemption(mint_space)
.await?;
let create_mint_transaction = Transaction::new_signed_with_payer(
&[
create_account(
&fee_payer.pubkey(), // Account funding account creation.
&mint.pubkey(), // New mint account to create.
mint_rent, // Lamports funding the mint account rent.
mint_space as u64, // Account size in bytes for the mint plus PermissionedBurnConfig.
&TOKEN_2022_PROGRAM_ID, // Program that owns the mint account.
),
permissioned_burn_ix::initialize(
&TOKEN_2022_PROGRAM_ID, // Token program that owns the mint.
&mint.pubkey(), // Mint account that stores the PermissionedBurnConfig extension.
&burn_authority.pubkey(), // Authority required to co-sign every burn for the mint.
)?,
initialize_mint(
&TOKEN_2022_PROGRAM_ID, // Program that owns the mint account.
&mint.pubkey(), // Mint account to initialize.
&fee_payer.pubkey(), // Authority allowed to mint new tokens.
Some(&fee_payer.pubkey()), // Authority allowed to freeze token accounts.
0, // Number of decimals for the token.
)?,
],
Some(&fee_payer.pubkey()),
&[&fee_payer, &mint],
client.get_latest_blockhash().await?,
);
client
.send_and_confirm_transaction(&create_mint_transaction)
.await?;
let source_token = get_associated_token_address_with_program_id(
&fee_payer.pubkey(),
&mint.pubkey(),
&TOKEN_2022_PROGRAM_ID,
);
let create_token_account_transaction = Transaction::new_signed_with_payer(
&[
create_associated_token_account(
&fee_payer.pubkey(), // Account funding the associated token account creation.
&fee_payer.pubkey(), // Owner of the associated token account.
&mint.pubkey(), // Mint for the associated token account.
&TOKEN_2022_PROGRAM_ID, // Token program that owns the token account.
),
mint_to_checked(
&TOKEN_2022_PROGRAM_ID, // Token program that owns the mint and token account.
&mint.pubkey(), // Mint account that issues the tokens.
&source_token, // Token account receiving the newly minted tokens.
&fee_payer.pubkey(), // Signer authorized to mint new tokens.
&[&fee_payer.pubkey()], // Additional multisig signers.
2, // Token amount in base units.
0, // Decimals defined on the mint.
)?,
],
Some(&fee_payer.pubkey()),
&[&fee_payer],
client.get_latest_blockhash().await?,
);
client
.send_and_confirm_transaction(&create_token_account_transaction)
.await?;
let standard_burn_ix = burn_checked(
&TOKEN_2022_PROGRAM_ID, // Token program that processes the burn.
&source_token, // Token account to burn from.
&mint.pubkey(), // Mint with the permissioned burn configuration.
&fee_payer.pubkey(), // Token account owner signing the burn.
&[&fee_payer.pubkey()], // Additional multisig signers.
1, // Token amount in base units.
0, // Decimals defined on the mint.
)?;
let standard_burn_transaction = Transaction::new_signed_with_payer(
&[standard_burn_ix],
Some(&fee_payer.pubkey()),
&[&fee_payer],
client.get_latest_blockhash().await?,
);
let standard_burn_result = client
.simulate_transaction(&standard_burn_transaction)
.await?;
let standard_burn_failure = standard_burn_result
.value
.err
.ok_or_else(|| anyhow!("Expected the standard burn to fail"))?;
let permissioned_burn_instruction = permissioned_burn_ix::burn_checked(
&TOKEN_2022_PROGRAM_ID, // Token program that processes the burn.
&source_token, // Token account to burn from.
&mint.pubkey(), // Mint with the permissioned burn configuration.
&burn_authority.pubkey(), // Burn authority co-signing the burn.
&fee_payer.pubkey(), // Token account owner signing the burn.
&[], // Additional multisig signers.
1, // Token amount in base units.
0, // Decimals defined on the mint.
)?;
let permissioned_burn_transaction = Transaction::new_signed_with_payer(
&[permissioned_burn_instruction],
Some(&fee_payer.pubkey()),
&[&fee_payer, &burn_authority],
client.get_latest_blockhash().await?,
);
client
.send_and_confirm_transaction(&permissioned_burn_transaction)
.await?;
let disable_authority_transaction = Transaction::new_signed_with_payer(
&[
set_authority(
&TOKEN_2022_PROGRAM_ID, // Token program that owns the mint.
&mint.pubkey(), // Mint with the permissioned burn configuration.
None, // Setting the authority to None disables permissioned burning.
AuthorityType::PermissionedBurn, // Authority type to update.
&burn_authority.pubkey(), // Current burn authority signing the update.
&[], // Additional multisig signers.
)?,
],
Some(&fee_payer.pubkey()),
&[&fee_payer, &burn_authority],
client.get_latest_blockhash().await?,
);
client
.send_and_confirm_transaction(&disable_authority_transaction)
.await?;
let standard_burn_after_disable_transaction = Transaction::new_signed_with_payer(
&[
burn_checked(
&TOKEN_2022_PROGRAM_ID, // Token program that processes the burn.
&source_token, // Token account to burn from.
&mint.pubkey(), // Mint with permissioned burning disabled.
&fee_payer.pubkey(), // Token account owner signing the burn.
&[&fee_payer.pubkey()], // Additional multisig signers.
1, // Token amount in base units.
0, // Decimals defined on the mint.
)?,
],
Some(&fee_payer.pubkey()),
&[&fee_payer],
client.get_latest_blockhash().await?,
);
client
.send_and_confirm_transaction(&standard_burn_after_disable_transaction)
.await?;
let source_account = client.get_account(&source_token).await?;
let source_state = StateWithExtensions::<Account>::unpack(&source_account.data)?;
let mint_account = client.get_account(&mint.pubkey()).await?;
let mint_state = StateWithExtensions::<Mint>::unpack(&mint_account.data)?;
let permissioned_burn_config = mint_state.get_extension::<PermissionedBurnConfig>()?;
println!("Mint Address: {}", mint.pubkey());
println!(
"Error From Failed Standard Burn: {:?}",
standard_burn_failure
);
println!(
"Source Amount After Burns: {}",
u64::from(source_state.base.amount)
);
println!("Extension After Disable: {:?}", permissioned_burn_config);
Ok(())
}

Is this page helpful?

Table of Contents

Edit Page
©️ 2026 Solana 基金会版权所有