How to use the Permanent Delegate extension

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.

Warning

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.

// Client
console.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.

Info

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 wallet
const payer = pg.wallet.keypair;

// Connection to devnet cluster
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

// Transaction signature returned from sent transaction
let 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 Account
const mintKeypair = Keypair.generate();
// Address for Mint Account
const mint = mintKeypair.publicKey;
// Decimals for Mint Account
const decimals = 2;
// Authority that can mint new tokens
const mintAuthority = pg.wallet.publicKey;
// Authority that can transfer or burn from any token account
const 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 extension
const mintLen = getMintLen([ExtensionType.PermanentDelegate]);
// Minimum lamports required for Mint Account
const 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 account
const createAccountInstruction = SystemProgram.createAccount({
  fromPubkey: payer.publicKey, // Account that will transfer lamports to created account
  newAccountPubkey: mint, // Address of the account to create
  space: mintLen, // Amount of bytes to allocate to the created account
  lamports, // Amount of lamports transferred to created account
  programId: 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 Extension
const initializePermanentDelegateInstruction =
  createInitializePermanentDelegateInstruction(
    mint, // Mint Account address
    permanentDelegate, // Designated Permanent Delegate
    TOKEN_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 data
const initializeMintInstruction = createInitializeMintInstruction(
  mint, // Mint Account Address
  decimals, // Decimals of Mint
  mintAuthority, // Designated Mint Authority
  null, // Optional Freeze Authority
  TOKEN_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 transaction
const transaction = new Transaction().add(
  createAccountInstruction,
  initializePermanentDelegateInstruction,
  initializeMintInstruction,
);

// Send transaction
transactionSignature = 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 Account
const randomKeypair = new Keypair();
// Create Token Account for random keypair
const sourceTokenAccount = await createAccount(
  connection,
  payer, // Payer to create Token Account
  mint, // Mint Account address
  randomKeypair.publicKey, // Token Account owner
  undefined, // Optional keypair, default to Associated Token Account
  undefined, // Confirmation options
  TOKEN_2022_PROGRAM_ID, // Token Extension Program ID
);

Next, create a destinationTokenAccount owned by the Playground wallet.

// Create Token Account for Playground wallet
const destinationTokenAccount = await createAccount(
  connection,
  payer, // Payer to create Token Account
  mint, // Mint Account address
  payer.publicKey, // Token Account owner
  undefined, // Optional keypair, default to Associated Token Account
  undefined, // Confirmation options
  TOKEN_2022_PROGRAM_ID, // Token Extension Program ID
);

Lastly, mint 2 tokens to the sourceTokenAccount to fund it.

// Mint tokens to sourceTokenAccount
transactionSignature = await mintTo(
  connection,
  payer, // Transaction fee payer
  mint, // Mint Account address
  sourceTokenAccount, // Mint to
  mintAuthority, // Mint Authority address
  200, // Amount
  undefined, // Additional signers
  undefined, // Confirmation options
  TOKEN_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 destination
transactionSignature = await transferChecked(
  connection,
  payer, // Transaction fee payer
  sourceTokenAccount, // Transfer from
  mint, // Mint Account address
  destinationTokenAccount, // Transfer to
  permanentDelegate, // Use Permanent Delegate as owner
  100, // Amount
  decimals, // Mint Account decimals
  undefined, // Additional signers
  undefined, // Confirmation options
  TOKEN_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 account
transactionSignature = await burnChecked(
  connection,
  payer, // Transaction fee payer
  sourceTokenAccount, // Burn from
  mint, // Mint Account address
  permanentDelegate, // Use Permanent Delegate as owner
  100, // Amount
  decimals, // Mint Account decimals
  undefined, // Additional signers
  undefined, // Confirmation options
  TOKEN_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.

Success

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.