The PermanentDelegate
extension allows for a designated Permanent Delegate for
a Mint Account. This permanent delegate has unrestricted delegate privileges
over all Token Accounts for that mint, enabling them to burn or transfer tokens
without limitation.
In this guide, we'll walk through an example of creating a token with the Permanent Delegate using Solana Playground. Here is the final script that this guide will walkthrough.
Understanding the Implications
This is a very powerful feature, and its implications have to be clearly stated for both users and app developers.
The Permanent Delegate is effectively a global owner of all Token Accounts for the mint. Due to the unlimited powers of the Permanent Delegate, if the delegate's keys are compromised, an attacker will have complete control over all Token Accounts for that mint.
Getting Started
Start by opening this Solana Playground link with the following starter code.
// Clientconsole.log("My address:", pg.wallet.publicKey.toString());const balance = await pg.connection.getBalance(pg.wallet.publicKey);console.log(`My balance: ${balance / web3.LAMPORTS_PER_SOL} SOL`);
If it is your first time using Solana Playground, you'll first need to create a Playground Wallet and fund the wallet with devnet SOL.
If you do not have a Playground wallet, you may see a type error within the
editor on all declarations of pg.wallet.publicKey
. This type error will clear
after you create a Playground wallet.
To get devnet SOL, run the solana airdrop
command in the Playground's
terminal, or visit this devnet faucet.
solana airdrop 5
Once you've created and funded the Playground wallet, click the "Run" button to run the starter code.
Add Dependencies
Lets start by setting up our script. We'll be using the @solana/web3.js
and
@solana/spl-token
libraries.
Replace the starter code with the following:
import {Connection,Keypair,SystemProgram,Transaction,clusterApiUrl,sendAndConfirmTransaction,} from "@solana/web3.js";import {ExtensionType,TOKEN_2022_PROGRAM_ID,createInitializePermanentDelegateInstruction,createInitializeMintInstruction,getMintLen,createAccount,mintTo,transferChecked,burnChecked,} from "@solana/spl-token";// Playground walletconst payer = pg.wallet.keypair;// Connection to devnet clusterconst connection = new Connection(clusterApiUrl("devnet"), "confirmed");// Transaction signature returned from sent transactionlet transactionSignature: string;
Mint Setup
First, lets define the properties of the Mint Account we'll be creating in the following step.
// Generate new keypair for Mint Accountconst mintKeypair = Keypair.generate();// Address for Mint Accountconst mint = mintKeypair.publicKey;// Decimals for Mint Accountconst decimals = 2;// Authority that can mint new tokensconst mintAuthority = pg.wallet.publicKey;// Authority that can transfer or burn from any token accountconst permanentDelegate = pg.wallet.publicKey;
Next, lets determine the size of the new Mint Account and calculate the minimum lamports needed for rent exemption.
// Size of Mint Account with extensionconst mintLen = getMintLen([ExtensionType.PermanentDelegate]);// Minimum lamports required for Mint Accountconst lamports = await connection.getMinimumBalanceForRentExemption(mintLen);
With Token Extensions, the size of the Mint Account will vary based on the extensions enabled.
Build Instructions
We will need to build a set of instructions to:
- Create a new account
- Initialize the
PermanentDelegate
extension - Initialize the remaining Mint Account data
First, build the instruction to invoke the System Program to create an account and assign ownership to the Token Extensions Program.
// Instruction to invoke System Program to create new accountconst createAccountInstruction = SystemProgram.createAccount({fromPubkey: payer.publicKey, // Account that will transfer lamports to created accountnewAccountPubkey: mint, // Address of the account to createspace: mintLen, // Amount of bytes to allocate to the created accountlamports, // Amount of lamports transferred to created accountprogramId: TOKEN_2022_PROGRAM_ID, // Program assigned as owner of created account});
Next, build the instruction to initialize the PermanentDelegate
extension for
the Mint Account.
// Instruction to initialize the PermanentDelegate Extensionconst initializePermanentDelegateInstruction =createInitializePermanentDelegateInstruction(mint, // Mint Account addresspermanentDelegate, // Designated Permanent DelegateTOKEN_2022_PROGRAM_ID, // Token Extension Program ID);
Lastly, build the instruction to initialize the rest of the Mint Account data. This is the same as with the original Token Program.
// Instruction to initialize Mint Account dataconst initializeMintInstruction = createInitializeMintInstruction(mint, // Mint Account Addressdecimals, // Decimals of MintmintAuthority, // Designated Mint Authoritynull, // Optional Freeze AuthorityTOKEN_2022_PROGRAM_ID, // Token Extension Program ID);
Send Transaction
Now add the instructions to a new transaction and send it to the network. This
will create a Mint Account with the PermanentDelegate
extension enabled.
// Add instructions to new transactionconst transaction = new Transaction().add(createAccountInstruction,initializePermanentDelegateInstruction,initializeMintInstruction,);// Send transactiontransactionSignature = await sendAndConfirmTransaction(connection,transaction,[payer, mintKeypair], // Signers);console.log("\nCreate Mint Account:",`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`,);
Run the script by clicking the Run
button. You can then inspect the
transaction on the SolanaFM.
Create Token Accounts
Next, we will set up two Token Accounts to demonstrate the functionality of the Permanent Delegate.
First, generate a random keypair and use it as the owner of a
sourceTokenAccount
.
// Random keypair to use as owner of Token Accountconst randomKeypair = new Keypair();// Create Token Account for random keypairconst sourceTokenAccount = await createAccount(connection,payer, // Payer to create Token Accountmint, // Mint Account addressrandomKeypair.publicKey, // Token Account ownerundefined, // Optional keypair, default to Associated Token Accountundefined, // Confirmation optionsTOKEN_2022_PROGRAM_ID, // Token Extension Program ID);
Next, create a destinationTokenAccount
owned by the Playground wallet.
// Create Token Account for Playground walletconst destinationTokenAccount = await createAccount(connection,payer, // Payer to create Token Accountmint, // Mint Account addresspayer.publicKey, // Token Account ownerundefined, // Optional keypair, default to Associated Token Accountundefined, // Confirmation optionsTOKEN_2022_PROGRAM_ID, // Token Extension Program ID);
Lastly, mint 2 tokens to the sourceTokenAccount
to fund it.
// Mint tokens to sourceTokenAccounttransactionSignature = await mintTo(connection,payer, // Transaction fee payermint, // Mint Account addresssourceTokenAccount, // Mint tomintAuthority, // Mint Authority address200, // Amountundefined, // Additional signersundefined, // Confirmation optionsTOKEN_2022_PROGRAM_ID, // Token Extension Program ID);console.log("\nMint Tokens:",`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`,);
Transfer with Permanent Delegate
Next, lets send a transaction to transfer 1 token from the sourceTokenAccount
to the destinationTokenAccount
. Remember, the sourceTokenAccount
is owned by
a randomly generated keypair.
To transfer tokens using the Permanent Delegate, use the transferChecked
instruction and specify the Permanent Delegate as the owner of the
sourceTokenAccount
.
// Transfer tokens from source to destinationtransactionSignature = await transferChecked(connection,payer, // Transaction fee payersourceTokenAccount, // Transfer frommint, // Mint Account addressdestinationTokenAccount, // Transfer topermanentDelegate, // Use Permanent Delegate as owner100, // Amountdecimals, // Mint Account decimalsundefined, // Additional signersundefined, // Confirmation optionsTOKEN_2022_PROGRAM_ID, // Token Extension Program ID);console.log("\nTransfer Tokens:",`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`,);
Burn with Permanent Delegate
Lets also send a transaction to burn 1 token from the sourceTokenAccount
.
To burn tokens using the Permanent Delegate
, use the burnChecked
instruction
and specify the Permanent Delegate as the owner of the sourceTokenAccount
.
// Burn tokens from token accounttransactionSignature = await burnChecked(connection,payer, // Transaction fee payersourceTokenAccount, // Burn frommint, // Mint Account addresspermanentDelegate, // Use Permanent Delegate as owner100, // Amountdecimals, // Mint Account decimalsundefined, // Additional signersundefined, // Confirmation optionsTOKEN_2022_PROGRAM_ID, // Token Extension Program ID);console.log("\nBurn Tokens:",`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`,);
Run the script by clicking the Run
button. You can then inspect the
transactions on the SolanaFM.
Note that both the transfer and burn transactions complete successfully, even though the transactions are not signed by the owner of the Token Account.
Conclusion
The PermanentDelegate
extension is a powerful extension that enables
developers to have much greater control over tokens they create, such as the
ability to retrieve tokens that have been mistakenly transferred. While this
extension offers greater flexibility, it's essential for users to be aware of
the implications of holding tokens with this extension enabled, particularly the
risks associated with compromised delegate keys.