Summary #
- The permanent delegate holds global ownership over all token accounts associated with the mint
- The permanent delegate has unrestricted permissions to transfer and burn tokens from any token account of that mint
- This delegate role designates a trusted entity with comprehensive control. Common use cases include sanction compliance and revocable access tokens.
- With this level of access, the permanent delegate can carry out high-level administrative functions, such as reassigning tokens, managing token supplies, and directly implementing specific policies or rules on the token accounts.
Overview #
The permanent delegate
extension allows a permanent delegate
for all tokens
of the mint. This means one address is capable of transferring or burning any
token of that mint, from any token account. This makes the extension very
powerful but can also be very risky. It gives a single address complete control
over the token supply. This can be good for things like automatic payments,
recovering drained wallets, and refunds. However, it's a double-edged sword, the
permanent delegate
could be stolen or abused. In the words of Uncle Ben, "With
great power, comes great responsibility."
Imagine a Solana based AirBnb, where NFTs that use permanent delegate are used
as the keys to unlock the door. When you check-in, the NFT key will be
transferred to you and you'll be able to enjoy your stay. At the end of your
stay, the owner will transfer it back from you to them - since they are the
permanent delegate
. What happens if your wallet gets drained, or you lose
access to the key? No worries, the owner can transfer the key from any account
back to you! But on the other end, say the owner doesn't want you staying there
anymore, they can revoke it at any time, and you'd be locked out. In this way,
the permanent delegate extension is a double-edged sword.
This all being said - the permanent delegate
is a very exciting extension that
adds a world of possibilities to Solana tokens.
Initializing a permanent delegate to mint #
Initializing a permanent delegate token involves three instructions:
SystemProgram.createAccount
createInitializePermanentDelegateInstruction
createInitializeMintInstruction
The first instruction SystemProgram.createAccount
allocates space on the
blockchain for the mint account. This instruction accomplishes three things:
- Allocates space
- Transfers lamports for rent
- Assigns to its owning program
SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: mint,
space: mintLen,
lamports: mintLamports,
programId: TOKEN_2022_PROGRAM_ID,
}),
The second instruction createInitializePermanentDelegateInstruction
initializes the permanent delegate extension. The defining argument that
dictates the permanent delegate will be a variable we create named
permanentDelegate
.
createInitializePermanentDelegateInstruction(
mint,
permanentDelegate.publicKey,
TOKEN_2022_PROGRAM_ID,
),
The third instruction createInitializeMintInstruction
initializes the mint.
createInitializeMintInstruction(
mint,
decimals,
mintAuthority.publicKey,
null,
TOKEN_2022_PROGRAM_ID,
);
When the transaction with these three instructions is sent, a new permanent delegate token is created with the specified configuration.
Transferring tokens as delegate #
The transferChecked
function enables the permanent delegate to securely
transfer tokens between accounts. This function makes sure that the token
transfer adheres to the mint's configured rules and requires the delegate to
sign the transaction.
/**
* Approve a delegate to transfer up to a maximum number of tokens from an account, asserting the token mint and decimals
*
* @param connection Connection to use
* @param payer Payer of the transaction fees
* @param mint Address of the mint
* @param account Address of the account
* @param delegate Account authorized to perform a transfer tokens from the source account
* @param owner Owner of the source account
* @param amount Maximum number of tokens the delegate may transfer
* @param decimals Number of decimals in approved amount
* @param multiSigners Signing accounts if `owner` is a multisig
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
*
* @return Signature of the confirmed transaction
*/
await transferChecked(
connection,
payer,
bobAccount,
mint,
carolAccount,
permanentDelegate,
amountToTransfer,
decimals,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
Burning tokens as delegate #
The burnChecked
function allows the permanent delegate to burn tokens from any
token account of the mint. This function makes sure that the burn operation
complies with the mint's rules and requires the delegate to sign the
transaction.
/**
* Burn tokens from an account, asserting the token mint and decimals
*
* @param connection Connection to use
* @param payer Payer of the transaction fees
* @param account Account to burn tokens from
* @param mint Mint for the account
* @param owner Account owner
* @param amount Amount to burn
* @param decimals Number of decimals in amount to burn
* @param multiSigners Signing accounts if `owner` is a multisig
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
*
* @return Signature of the confirmed transaction
*/
await burnChecked(
connection,
payer,
bobAccount,
mint,
permanentDelegate,
amountToBurn,
decimals,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
Assign permissions to a new delegate #
The approveChecked
function approves a delegate to transfer or burn up to a
maximum number of tokens from an account. This allows the designated delegate to
perform token transfers on behalf of the account owner up to the specified
limit.
/**
* Approve a delegate to transfer up to a maximum number of tokens from an account, asserting the token mint and
* decimals
*
* @param connection Connection to use
* @param payer Payer of the transaction fees
* @param mint Address of the mint
* @param account Address of the account
* @param delegate Account authorized to perform a transfer tokens from the source account
* @param owner Owner of the source account
* @param amount Maximum number of tokens the delegate may transfer
* @param decimals Number of decimals in approved amount
* @param multiSigners Signing accounts if `owner` is a multisig
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
*
* @return Signature of the confirmed transaction
*/
// Approve new delegate to perform actions
await approveChecked(
connection,
payer,
mint,
bobAccount,
delegate.publicKey,
bob,
amountToApprove,
decimals,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
// Newly assigned delegate can now transfer from an account
await transferChecked(
connection,
payer,
bobAccount,
mint,
carolAccount,
carol,
amountToTransfer,
decimals,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
Lab #
In this lab, we'll explore the functionality of the permanent delegate
extension by creating a mint account with a permanent delegate and testing
various interactions with token accounts associated with that mint.
1. Setup Environment #
To get started, create an empty directory named permanent-delegate
and
navigate to it. We'll be initializing a brand new project. Run npm init
and
follow through the prompts.
Next, we'll need to add our dependencies. Run the following to install the required packages:
npm i @solana-developers/helpers @solana/spl-token @solana/web3.js esrun
Create a directory named src
. In this directory, create a file named
index.ts
. This is where we will run checks against the rules of this
extension. Paste the following code in index.ts
:
import {
sendAndConfirmTransaction,
Connection,
Keypair,
SystemProgram,
Transaction,
PublicKey,
} from "@solana/web3.js";
import {
ExtensionType,
createInitializeMintInstruction,
createInitializePermanentDelegateInstruction,
mintTo,
createAccount,
getMintLen,
TOKEN_2022_PROGRAM_ID,
transferChecked,
} from "@solana/spl-token";
import { initializeKeypair } from "@solana-developers/helpers";
const connection = new Connection("http://127.0.0.1:8899", "confirmed");
const payer = await initializeKeypair(connection);
const mintAuthority = payer;
const mintKeypair = Keypair.generate();
const mint = mintKeypair.publicKey;
const permanentDelegate = payer;
const extensions = [ExtensionType.PermanentDelegate];
const mintLen = getMintLen(extensions);
const decimals = 9;
const amountToMint = 100;
const amountToTransfer = 10;
const amountToBurn = 5;
// Create mint account with permanent delegate
// Create delegate and destination token accounts
// Mint tokens to accounts
// Attempt to transfer with correct delegate
// Attempt to transfer without correct delegate
// Attempt to transfer from one account to another with correct delegate
// Attempt to burn with correct delegate
// Attempt to burn without correct delegate
// Grant permission to an account to transfer tokens from a different token account
// Try to transfer tokens again with Carol as the delegate, overdrawing her allotted control
index.ts
creates a connection to the specified validator node and calls
initializeKeypair
. It also has a few variables we will be using in the rest of
this lab. The index.ts
is where we'll end up calling the rest of our script
once we've written it.
If you run into an error in initializeKeypair
with airdropping, follow the
next step.
2. Run validator node #
For the sake of this guide, we'll be running our own validator node.
In a separate terminal, run the following command: solana-test-validator
. This
will run the node and also log out some keys and values. The value we need to
retrieve and use in our connection is the JSON RPC URL, which in this case is
http://127.0.0.1:8899
. We then use that in the connection to specify to use
the local RPC URL.
const connection = new Connection("http://127.0.0.1:8899", "confirmed");
Alternatively, if you'd like to use testnet or devnet, import the
clusterApiUrl
from @solana/web3.js
and pass it to the connection as such:
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
3. Helpers #
When we pasted the index.ts
code from earlier, we added the following helpers:
initializeKeypair
: This function creates the keypair for thepayer
and also airdrops some SOL to itmakeKeypairs
: This function creates keypairs without airdropping any SOL
Additionally we have some initial accounts and variables that will be used to
test the permanent delegate
extension!
4. Create Mint with permanent delegate #
When creating a mint token with default state, we must create the account instruction, initialize the default account state for the mint account and initialize the mint itself.
Create an asynchronous function named
createTokenExtensionMintWithPermanentDelegate
in src/mint-helper.ts
. This
function will create the mint such that all new mints will be created with a
permanent delegate. The function will take the following arguments:
connection
: The connection objectpayer
: Payer for the transactionmintKeypair
: Keypair for the new mintdecimals
: Mint decimalspermanentDelegate
: Assigned delegate keypair
The first step in creating a mint is reserving space on Solana with the
SystemProgram.createAccount
method. This requires specifying the payer's
keypair, (the account that will fund the creation and provide SOL for rent
exemption), the new mint account's public key (mintKeypair.publicKey
), the
space required to store the mint information on the blockchain, the amount of
SOL (lamports) necessary to exempt the account from rent and the ID of the token
program that will manage this mint account (TOKEN_2022_PROGRAM_ID
).
const extensions = [ExtensionType.PermanentDelegate];
const mintLen = getMintLen(extensions);
const mintLamports =
await connection.getMinimumBalanceForRentExemption(mintLen);
const createAccountInstruction = SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: mint,
space: mintLen,
lamports: mintLamports,
programId: TOKEN_2022_PROGRAM_ID,
});
After the mint account creation, the next step involves initializing it with a
permanent delegate. The createInitializePermanentDelegateInstruction
function
is used to generate an instruction that enables the mint to set the permanent
delegate of any new mint accounts.
const initializePermanentDelegateInstruction =
createInitializePermanentDelegateInstruction(
mint,
permanentDelegate.publicKey,
TOKEN_2022_PROGRAM_ID,
);
Next, let's add the mint instruction by calling
createInitializeMintInstruction
and passing in the required arguments. This
function is provided by the SPL Token package and it constructs a transaction
instruction that initializes a new mint.
const initializeMintInstruction = createInitializeMintInstruction(
mint,
decimals,
mintAuthority.publicKey, // Designated Mint Authority
null, // No Freeze Authority
TOKEN_2022_PROGRAM_ID,
);
Lastly, let's add all of the instructions to a transaction and send it to the blockchain:
const transaction = new Transaction().add(
createAccountInstruction,
initializePermanentDelegateInstruction,
initializeMintInstruction,
);
return await sendAndConfirmTransaction(connection, transaction, [
payer,
mintKeypair,
]);
Putting it all together, the final src/mint-helper.ts
file will look like
this:
import {
ExtensionType,
TOKEN_2022_PROGRAM_ID,
createInitializeMintInstruction,
createInitializePermanentDelegateInstruction,
getMintLen,
} from "@solana/spl-token";
import {
Connection,
Keypair,
SystemProgram,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
/**
* Creates the mint with a permanent delegate
* @param connection
* @param payer
* @param mintKeypair
* @param decimals
* @param permanentDelegate
* @returns signature of the transaction
*/
export async function createTokenExtensionMintWithPermanentDelegate(
connection: Connection,
payer: Keypair,
mintKeypair: Keypair,
decimals: number = 2,
permanentDelegate: Keypair,
): Promise<string> {
const mintAuthority = payer;
const mint = mintKeypair.publicKey;
const extensions = [ExtensionType.PermanentDelegate];
const mintLen = getMintLen(extensions);
const mintLamports =
await connection.getMinimumBalanceForRentExemption(mintLen);
const createAccountInstruction = SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: mint,
space: mintLen,
lamports: mintLamports,
programId: TOKEN_2022_PROGRAM_ID,
});
const initializePermanentDelegateInstruction =
createInitializePermanentDelegateInstruction(
mint,
permanentDelegate.publicKey,
TOKEN_2022_PROGRAM_ID,
);
const initializeMintInstruction = createInitializeMintInstruction(
mint,
decimals,
mintAuthority.publicKey, // Designated Mint Authority
null, // No Freeze Authority
TOKEN_2022_PROGRAM_ID,
);
const transaction = new Transaction().add(
createAccountInstruction,
initializePermanentDelegateInstruction,
initializeMintInstruction,
);
return await sendAndConfirmTransaction(connection, transaction, [
payer,
mintKeypair,
]);
}
6. Create printBalances function #
We're going to be creating multiple tests that modify a token account's balance. To make it easier to follow along we should probably create a utility function that prints all token account balances.
At the bottom of the src/index.ts
file add the following printBalances
function:
async function printBalances(
connection: Connection,
tokenAccounts: PublicKey[],
names: string[],
) {
if (tokenAccounts.length !== names.length)
throw new Error("Names needs to be one to one with accounts");
for (let i = 0; i < tokenAccounts.length; i++) {
const tokenInfo = await getAccount(
connection,
tokenAccounts[i],
undefined,
TOKEN_2022_PROGRAM_ID,
);
console.log(`${names[i]}: ${tokenInfo.amount}`);
}
}
7. Test Setup #
Now that we have the ability to create a mint with a permanent delegate for all of its new mint accounts, let's write some tests to see how it functions.
7.1 Create Mint with Permanent Delegate #
Let's first create a mint with payer
as the permanent delegate. To do this we
call the createTokenExtensionMintWithPermanentDelegate
function we just
created in our index.ts
file:
// Create mint account with permanent delegate
await createTokenExtensionMintWithPermanentDelegate(
connection,
payer, // Also known as alice
mintKeypair,
decimals,
defaultState,
);
7.2 Create Test Token Accounts #
Now, let's create three new Token accounts to test with. We can accomplish this
by calling the createAccount
helper provided by the SPL Token library. We will
use the keypairs we generated at the beginning: alice
, bob
, and carol
.
In this lab, alice
will be the permanent delegate.
// Create delegate and destination token accounts
const aliceAccount = await createAccount(
connection,
payer,
mint,
alice.publicKey,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
const bobAccount = await createAccount(
connection,
payer,
mint,
bob.publicKey,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
const carolAccount = await createAccount(
connection,
payer,
mint,
carol.publicKey,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
7.3 Mint tokens to accounts #
In the previous step, we created the 3 accounts we need to test the
permanent delegate
extension. Next, we need to mint tokens to those accounts
before we write the tests.
Add the tokenAccounts
and names
variables and then create a for loop that
iterates over each account and mints 100 tokens to each account. Call the
printBalances
function so we can display the token balance of each account:
// Mint tokens to accounts
const tokenAccounts = [aliceAccount, bobAccount, carolAccount];
const names = ["Alice", "Bob", "Carol"];
for (const holder of tokenAccounts) {
await mintTo(
connection,
payer,
mint,
holder,
mintAuthority,
amountToMint,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
}
console.log("Initial Balances: ");
await printBalances(connection, tokenAccounts, names);
Start your local validator and run npx esrun src/index.ts
. You should see the
following in your terminal, indicating that our token accounts have had tokens
minted to them:
Initial Balances:
Alice: 100
Bob: 100
Carol: 100
8. Tests #
Now let's write some tests to show the interactions that can be had with the
permanent delegate
extension.
We'll write the following tests:
-
Attempt to Transfer with Correct Delegate:
- Have Alice transfer tokens from Bob's account to herself successfully since she is the permanent delegate.
- Print balances to verify the transfer.
-
Attempt to Transfer without Correct Delegate:
- Have Bob attempt to transfer tokens from Alice's account to himself (expect this to fail since Bob isn't authorized).
- Print balances to verify the failure.
-
Attempt to Transfer from One Account to Another with Correct Delegate:
- Have Alice transfer tokens from Bob's account to Carol's account.
- Print balances to verify the transfer.
-
Attempt to Burn with Correct Delegate:
- Have Alice burn tokens from Bob's account successfully since she is the permanent delegate.
- Print balances to verify the burning.
-
Attempt to Burn without Correct Delegate:
- Have Bob attempt to burn tokens from Carol's account (expect this to fail since Bob isn't authorized).
- Print balances to verify the failure.
-
Grant Permission to an Account to Transfer Tokens from a Different Token Account:
- Approve Carol to transfer tokens from Bob's account to herself.
- Transfer tokens from Bob's account to Carol's account.
- Print balances to verify the transfer.
-
Try to Transfer Tokens Again with Carol as the Delegate, Overdrawing Her Allotted Control:
- Attempt to transfer tokens from Bob's account to Carol's account with Carol again, but overdraw her allotted control (expect this to fail).
8.1 Transfer tokens with the correct delegate #
In this test, alice
attempts to transfer tokens from bob
to herself. This
test is expected to pass as alice
is the permanent delegate and has control
over the token accounts of that mint.
To do this, let's wrap a transferChecked
function in a try catch
and print
out the balances of our accounts:
// Attempt to transfer with correct delegate
{
// Have Alice transfer tokens from Bob to herself ( Will Succeed )
try {
await transferChecked(
connection,
payer,
bobAccount,
mint,
aliceAccount,
alice,
amountToTransfer,
decimals,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
console.log(
"✅ Since Alice is the permanent delegate, she has control over all token accounts of this mint",
);
await printBalances(connection, tokenAccounts, names);
} catch (error) {
console.log("Alice should be able to transfer Bob's tokens to Alice");
}
}
Test this by running the script:
npx esrun src/index.ts
We should see the following error logged out in the terminal, meaning the
extension is working as intended.
✅ Since Alice is the permanent delegate, she has control over all token accounts of this mint
8.2 Transfer tokens with incorrect delegate #
In this test, bob
is going to try to transfer tokens from alice
to himself.
Given that bob
is not the permanent delegate, the attempt won't be successful.
Similar to the previous test we can create this test by calling
transferChecked
and then printing the balances:
// Attempt to transfer without correct delegate
{
// Have Bob try to transfer tokens from Alice to himself ( Will Fail )
try {
await transferChecked(
connection,
payer,
aliceAccount, // transfer from
mint,
bobAccount,
bob, // incorrect delegate
amountToTransfer,
decimals,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
console.log("Bob should not be able to transfer tokens");
} catch (error) {
console.log(
"✅ We expect this to fail because Bob does not have authority over Alice's funds",
);
await printBalances(connection, tokenAccounts, names);
}
}
Go ahead and run the script, the transaction should fail.
npx esrun src/index.ts
8.3 Transfer from one account to another with the correct delegate #
Lets use the power of the permanent delegate extension to have alice
transfer
some tokens from bob
to carol
.
We expect this test to succeed. Remember, the permanent delegate has control over all token accounts of the mint.
To test this, let's wrap a transferChecked
function in a try catch
and print
the balances:
// Attempt to transfer from one account to another with correct delegate
{
// Have Alice transfer tokens from Bob to Carol
try {
await transferChecked(
connection,
payer,
bobAccount, // transfer from
mint,
carolAccount, // transfer to
alice,
amountToTransfer,
decimals,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
console.log(
"✅ Since Alice is the permanent delegate, she has control and can transfer Bob's tokens to Carol",
);
await printBalances(connection, tokenAccounts, names);
} catch (error) {
console.log("Alice should be able to transfer Bob's tokens to Alice");
}
}
In our first test we wrote, bob
had 10 of his tokens transferred to carol
.
Up until this point bob
has 90 tokens remaining. Run the test and see the
results. You will notice that bob
now has 80 tokens:
npx esrun src/index.ts
8.4 Burn with correct delegate #
Now let's try and burn some of the tokens from bob
. This test is expected to
pass.
We'll do this by calling burnChecked
and then printing out the balances:
// Attempt to burn with correct delegate
{
// Have Alice burn Bob's tokens
try {
await burnChecked(
connection,
payer,
bobAccount,
mint,
alice, // correct permanent delegate
amountToBurn, // in this case is 5
decimals,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
console.log(
"✅ Since Alice is the permanent delegate, she has control and can burn Bob's tokens",
);
await printBalances(connection, tokenAccounts, names);
} catch (error) {
console.error("Alice should be able to burn Bob's tokens");
}
}
Run the tests again:
npx esrun src/index.ts
Bob had 5 tokens burned and now only has 75 tokens. Poor Bob!
8.5 Burn with incorrect delegate #
Let's try and burn tokens from an account using the incorrect delegate. This is
expected to fail as bob
doesn't have any control over the token accounts.
// Attempt to burn without correct delegate
{
// Have Bob try to burn tokens from Carol ( Will Fail )
try {
await burnChecked(
connection,
payer,
carolAccount,
mint,
bob, // wrong permanent delegate
amountToBurn,
decimals,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
await printBalances(connection, tokenAccounts, names);
console.error("Bob should not be able to burn the tokens");
} catch (error) {
console.log(
"✅ We expect this to fail since Bob is not the permanent delegate and has no control over the tokens",
);
}
}
Run npm start
. You will see the following message, indicating that the
extension is working as intended:
✅ We expect this to fail since Bob is not the permanent delegate and has no control over the tokens
8.6. Assign delegate permissions to Carol and transfer #
With the permanent delegate
extension, the initial delegate can grant a token
account permission to hold a certain level of control over the mint tokens. In
this case, alice
will allow carol
to transfer some of the tokens from bob
account to herself.
For this to work we will need to set some boundaries for carol
. Using the
approveChecked
function provided by the SPL Library, we can set the maximum
number of tokens that can be transferred or burned by carol
. This ensures that
she can only transfer a specified amount, protecting the overall balance from
excessive or unauthorized transfers.
Add the following test:
// Grant permission to an account to transfer tokens from a different token account
{
// Approve Carol to transfer Bob's tokens to herself
await approveChecked(
connection,
payer,
mint,
bobAccount,
carol.publicKey,
bob,
amountToTransfer, // maximum amount to transfer
decimals,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
await transferChecked(
connection,
payer,
bobAccount,
mint,
carolAccount,
carol,
amountToTransfer,
decimals,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
console.log(
"✅ Since Alice is the permanent delegate, she can allow Carol to transfer Bob's tokens to Carol",
);
await printBalances(connection, tokenAccounts, names);
}
Run the tests again. You will notice that bob
now only has 65 tokens as
carol
has just transferred 10 of his tokens to herself:
npx esrun src/index.ts
8.7. Attempt to transfer again #
In the previous test, we approved carol
to be able to transfer 10 tokens to
herself. This means that she has reached the maximum amount of tokens to send
from another account. Let's write a test and attempt to transfer another 10
tokens to herself. This is expected to fail.
// Try to transfer tokens again with Carol as the delegate, overdrawing her allotted control
{
// Try to transfer again with Carol as the delegate overdrawing her allotted control
try {
await transferChecked(
connection,
payer,
bobAccount,
mint,
carolAccount,
carol, // Owner - whoever has the authority to transfer tokens on behalf of the destination account
amountToTransfer,
decimals,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
} catch (e) {
console.log(
`✅ We expect this to fail since Carol already transferred ${amountToTransfer} tokens and has no more allotted`,
);
}
}
Run the tests one last time and you will see this message, meaning that the
✅ We expect this to fail since Carol already transferred 10 tokens and has no more allotted
Thats it! You've just created a mint account with a permanent delegate and tested that the functionality all works!
Challenge #
Create your own mint account with a permanent delegate.