Kontrolowane Spalanie

Czym Jest Kontrolowane Spalanie?

Rozszerzenie mint PermissionedBurnConfig programu Token Extensions Program wymaga, aby skonfigurowany organ spalania współpodpisywał każde spalenie dla danego mintu.

Dopóki organ spalania jest ustawiony:

  • Standardowe instrukcje Burn i BurnChecked kończą się błędem TokenError::InvalidInstruction.
  • Spalanie musi odbywać się za pomocą PermissionedBurnInstruction::Burn lub PermissionedBurnInstruction::BurnChecked, podpisanych zarówno przez organ spalania, jak i właściciela token account lub jego delegata.

Organ spalania jest współsygnatariuszem, a nie zastępcą właściciela. Sam organ nie może spalać tokenów z cudzego token account, a właściciel token account nie może spalić tokenów bez podpisu organu. Stały delegat również musi korzystać z instrukcji kontrolowanego spalania i nadal potrzebuje współpodpisu organu spalania.

Pozwala to emitentowi synchronizować podaż tokenów z rejestrem off-chain — na przykład w przypadku tokenizowanego aktywa, które musi pozostawać w relacji 1:1 — poprzez uniemożliwienie posiadaczom jednostronnego spalania tokenów.

Organ spalania można później zmienić za pomocą SetAuthority używając AuthorityType::PermissionedBurn. Ustawienie organu na None wyłącza kontrolowane spalanie i ponownie włącza standardowe instrukcje spalania. Dane rozszerzenia pozostają na mincie.

Lokalny test validator zawiera również starszą wersję Token-2022, dlatego aby uruchomić przykłady z tej strony, załaduj program v11.0.0 z devnet do swojego test validator:

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

Jak utworzyć mint i spalanie z uprawnieniami

Aby utworzyć mint z uprawnionym spalaniem i spalić tokeny:

  1. Oblicz rozmiar mint account i rent potrzebny dla mint oraz rozszerzenia PermissionedBurnConfig.
  2. Utwórz mint account za pomocą CreateAccount, zainicjalizuj PermissionedBurnConfig i zainicjalizuj mint za pomocą InitializeMint.
  3. Utwórz token account i mintuj tokeny.
  4. Spal za pomocą PermissionedBurnInstruction::BurnChecked podpisanego przez właściciela token account oraz przez organ spalania.
  5. Opcjonalnie wyłącz uprawnione spalanie za pomocą SetAuthority używając AuthorityType::PermissionedBurn i uprawnienia None, co ponownie włącza standardowe instrukcje spalania. Poniższe pełne przykłady kodu pokazują ten krok.

Oblicz rozmiar konta

Oblicz rozmiar mint account dla podstawowego mint oraz rozszerzenia PermissionedBurnConfig. Jest to rozmiar używany w 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]));

Oblicz rent

Oblicz rent używając rozmiaru potrzebnego dla mint oraz rozszerzenia 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();

Utwórz mint account

Utwórz mint account z obliczoną przestrzenią i lamports.

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
})
]);

Zainicjalizuj PermissionedBurn

Zainicjalizuj rozszerzenie PermissionedBurnConfig na mint z organem spalania.

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
})
]);

Zainicjalizuj mint

Zainicjalizuj mint za pomocą InitializeMint w tej samej transakcji.

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
})
]);

Utwórz token account i wybij tokeny

Utwórz token account dla płatnika i wybij do niego tokeny.

Spalanie z uprawnieniem burn authority

Spalaj tokeny za pomocą PermissionedBurnInstruction::BurnChecked podpisanego zarówno przez właściciela token account, jak i burn authority.

Oblicz rozmiar konta

Oblicz rozmiar mint account dla podstawowego mint oraz rozszerzenia PermissionedBurnConfig. Jest to rozmiar używany w CreateAccount.

Oblicz rent

Oblicz rent używając rozmiaru potrzebnego dla mint oraz rozszerzenia PermissionedBurnConfig.

Utwórz mint account

Utwórz mint account z obliczoną przestrzenią i lamports.

Zainicjalizuj PermissionedBurn

Zainicjalizuj rozszerzenie PermissionedBurnConfig na mint z organem spalania.

Zainicjalizuj mint

Zainicjalizuj mint za pomocą InitializeMint w tej samej transakcji.

Utwórz token account i wybij tokeny

Utwórz token account dla płatnika i wybij do niego tokeny.

Spalanie z uprawnieniem burn authority

Spalaj tokeny za pomocą PermissionedBurnInstruction::BurnChecked podpisanego zarówno przez właściciela token account, jak i burn authority.

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]));

Kolejność instrukcji

PermissionedBurnInstruction::Initialize musi wystąpić przed InitializeMint. CreateAccount, PermissionedBurnInstruction::Initialize i InitializeMint muszą być zawarte w tej samej transakcji.

Źródła referencyjne

ElementOpisŹródło
PermissionedBurnConfigRozszerzenie minta przechowujące uprawnienie wymagane do współpodpisywania każdego spalenia dla minta.Źródło
PermissionedBurnInstruction::InitializeInstrukcja inicjalizująca konfigurację permissioned burn przed InitializeMint.Źródło
PermissionedBurnInstruction::BurnInstrukcja spalania wymagająca podpisów od burn authority oraz właściciela lub delegata token account.Źródło
PermissionedBurnInstruction::BurnCheckedInstrukcja spalania z weryfikacją liczby miejsc dziesiętnych, używająca tych samych kont co PermissionedBurnInstruction::Burn.Źródło
AuthorityType::PermissionedBurnDyskryminator uprawnień używany z SetAuthority do rotacji lub wyłączania burn authority na mincie.Źródło
process_initializeLogika procesora inicjalizująca PermissionedBurnConfig na niezainicjalizowanym mincie i przechowująca burn authority.Źródło
process_burnLogika procesora odrzucająca standardowe spalenia, gdy burn authority jest ustawione, i weryfikująca podpis uprawnienia.Źródło
process_set_authorityLogika procesora walidująca i rotująca burn authority, gdy używany jest AuthorityType::PermissionedBurn.Źródło

Typescript

Poniższy przykład Kit używa wygenerowanych instrukcji bezpośrednio. Opublikowany starszy pakiet @solana/spl-token nie obsługuje jeszcze uprzywilejowanego spalania, dlatego nie dołączono przykładu dla starszej wersji.

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?

Spis treści

Edytuj stronę