Summary #
- The
default state
extension enables developers to set new token accounts for a mint with this extension to be frozen by default, requiring interaction with a specific service to unfreeze and utilize the tokens. - There are three states of token accounts: Initialized, Uninitialized, and Frozen, which determine how a token account can be interacted with.
- When a token account is frozen, the balance cannot change.
- The
freezeAuthority
is the only address that can freeze and thaw a token account - The
default state
can be updated usingupdateDefaultAccountState
- The lab demonstrates creating a mint with the
default state
extension and creating a new token account which is set to a frozen state upon creation. The lab includes tests to ensure the extension works as intended for both minting and transferring tokens in frozen and thawed states.
Overview #
The default state
extension allows developers to force all new token accounts
to be in one of two states: "Initialized" or "Frozen". Most usefully, with this
extension any new token accounts created can be set to frozen. When a token
account is frozen, it's balance cannot change. Meaning it cannot be minted to,
transferred from or burned. Only the freezeAuthority
can thaw a frozen
account.
Imagine you're a Solana game dev, and you only want players of your game to
interact with your in-game token. You can make the player, sign up for the game
to thaw their token account and allow them to play and trade with other players.
This works because of the default state
extension, where it is set that all
new token accounts are frozen.
Types of States #
There are 3 types of state with the default account state extension:
- Uninitialized: This state indicates that the token account has been created but not yet initialized through the Token Program.
- Initialized: An account in the Initialized state has been properly set up through the Token Program. This means it has a specified mint and an owner has been assigned.
- Frozen: A Frozen account is one that has been temporarily disabled from performing certain operations, specifically transferring and minting tokens.
/** Token account state as stored by the program */
export enum AccountState {
Uninitialized = 0,
Initialized = 1,
Frozen = 2,
}
However, default state
only deals with the latter two: Initialized
and
Frozen
. When you freeze an account, the state is Frozen
, when you thaw, it
is Initialized
.
Adding default account state #
Initializing a mint with transfer fee involves three instructions:
SystemProgram.createAccount
createInitializeTransferFeeConfigInstruction
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 it's owning program
To grab the size of the mint account, we call getMintLen
, and to grab the
lamports needed for the space, we call getMinimumBalanceForRentExemption
.
const mintLen = getMintLen([ExtensionType.DefaultAccountState]);
// Minimum lamports required for Mint Account
const lamports = await connection.getMinimumBalanceForRentExemption(mintLen);
const createAccountInstruction = SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: mintKeypair.publicKey,
space: mintLen,
lamports,
programId: TOKEN_2022_PROGRAM_ID,
});
The second
instruction createInitializeDefaultAccountStateInstruction
initializes the
default account state extension.
const initializeDefaultAccountStateInstruction =
createInitializeDefaultAccountStateInstruction(
mintKeypair.publicKey, // Mint
defaultState, // Default State
TOKEN_2022_PROGRAM_ID,
);
The third instruction createInitializeMintInstruction
initializes the mint.
const initializeMintInstruction = createInitializeMintInstruction(
mintKeypair.publicKey,
decimals,
payer.publicKey,
payer.publicKey,
TOKEN_2022_PROGRAM_ID,
);
Lastly, add all of these instructions to a transaction and send it to the blockchain.
const transaction = new Transaction().add(
createAccountInstruction,
initializeDefaultAccountStateInstruction,
initializeMintInstruction,
);
return await sendAndConfirmTransaction(connection, transaction, [
payer,
mintKeypair,
]);
Updating the Default Account State #
You can always change the default account state assuming you have the authority
to do so. To do this, simply call updateDefaultAccountState
.
/**
* Update the default account state on a mint
*
* @param connection Connection to use
* @param payer Payer of the transaction fees
* @param mint Mint to modify
* @param state New account state to set on created accounts
* @param freezeAuthority Freeze authority of the mint
* @param multiSigners Signing accounts if `freezeAuthority` is a multisig
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
*
* @return Signature of the confirmed transaction
*/
export async function updateDefaultAccountState(
connection: Connection,
payer: Signer,
mint: PublicKey,
state: AccountState,
freezeAuthority: Signer | PublicKey,
multiSigners: Signer[] = [],
confirmOptions?: ConfirmOptions,
programId = TOKEN_2022_PROGRAM_ID,
): Promise<TransactionSignature>;
Updating the Freeze Authority #
Lastly, you may want to update the freezeAuthority
to another account. Say you
want to handle the freezing and thawing by a program for example. You can do
this, by calling setAuthority
, adding in the correct accounts and passing in
the authorityType
, which in this case would be AuthorityType.FreezeAccount
.
/**
* Assign a new authority to the account
*
* @param connection Connection to use
* @param payer Payer of the transaction fees
* @param account Address of the account
* @param currentAuthority Current authority of the specified type
* @param authorityType Type of authority to set
* @param newAuthority New authority of the account
* @param multiSigners Signing accounts if `currentAuthority` is a multisig
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
*
* @return Signature of the confirmed transaction
*/
await setAuthority(
connection,
payer,
mint,
currentAuthority,
AuthorityType.FreezeAccount,
newAuthority,
[],
undefined,
TOKEN_2022_PROGRAM_ID,
);
Lab #
In this lab we will be creating a mint which all new token accounts are frozen
upon creation by using the default state
extension. We will then write tests
to check if the extension is working as intended by attempting to mint and
transfer the tokens in a frozen and thawed account state.
1. Setup Environment #
To get started, create an empty directory named default-account-state
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 dotenv typescript
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 {
AccountState,
TOKEN_2022_PROGRAM_ID,
getAccount,
mintTo,
thawAccount,
transfer,
createAccount,
} from "@solana/spl-token";
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
// import { createTokenExtensionMintWithDefaultState } from "./mint-helper"; // This will be uncommented later
import { initializeKeypair, makeKeypairs } from "@solana-developers/helpers";
const connection = new Connection("http://127.0.0.1:8899", "confirmed");
const payer = await initializeKeypair(connection);
const [mintKeypair, ourTokenAccountKeypair, otherTokenAccountKeypair] =
makeKeypairs(3);
const mint = mintKeypair.publicKey;
const decimals = 2;
const defaultState = AccountState.Frozen;
const ourTokenAccount = ourTokenAccountKeypair.publicKey;
// To satisfy the transferring tests
const otherTokenAccount = otherTokenAccountKeypair.publicKey;
const amountToMint = 1000;
const amountToTransfer = 50;
// CREATE MINT WITH DEFAULT STATE
// CREATE TEST TOKEN ACCOUNTS
// TEST: MINT WITHOUT THAWING
// TEST: MINT WITH THAWING
// TEST: TRANSFER WITHOUT THAWING
// TEST: TRANSFER WITH THAWING
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");
If you decide to use devnet, and have issues with airdropping sol. Feel free to
add the keypairPath
parameter to initializeKeypair
. You can get this from
running solana config get
in your terminal. And then go to
faucet.solana.com and airdrop some sol to your
address. You can get your address from running solana address
in your
terminal.
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:
payer
: Used to pay for and be the authority for everythingmintKeypair
: Our mint that will have thedefault state
extensionourTokenAccountKeypair
: The token account owned by payer that we'll use for testingotherTokenAccountKeypair
: Another token used for testing
4. Create Mint with Default account state #
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 createTokenExtensionMintWithDefaultState
in src/mint-helpers.ts
. This function will create the mint such that all new
token accounts will be “frozen” to start. The function will take the following
arguments:
connection
: The connection objectpayer
: Payer for the transactionmintKeypair
: Keypair for the new mintdecimals
: Mint decimalsdefaultState
: Mint token default state - eg:AccountState.Frozen
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 mintLen = getMintLen([ExtensionType.DefaultAccountState]);
// Minimum lamports required for Mint Account
const lamports = await connection.getMinimumBalanceForRentExemption(mintLen);
const createAccountInstruction = SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: mintKeypair.publicKey,
space: mintLen,
lamports,
programId: TOKEN_2022_PROGRAM_ID,
});
After the mint account creation, the next step involves initializing it with the
default state. The createInitializeDefaultAccountStateInstruction
function is
used to generate an instruction that enables the mint to set defaultState
of
any new token accounts.
const initializeDefaultAccountStateInstruction =
createInitializeDefaultAccountStateInstruction(
mintKeypair.publicKey,
defaultState,
TOKEN_2022_PROGRAM_ID,
);
Next, lets 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(
mintKeypair.publicKey,
decimals,
payer.publicKey, // Designated Mint Authority
payer.publicKey, // Designated 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,
initializeDefaultAccountStateInstruction,
initializeMintInstruction,
);
return await sendAndConfirmTransaction(connection, transaction, [
payer,
mintKeypair,
]);
Putting it all together, the final src/mint-helpers.ts
file will look like
this:
import {
AccountState,
ExtensionType,
TOKEN_2022_PROGRAM_ID,
createInitializeDefaultAccountStateInstruction,
createInitializeMintInstruction,
getMintLen,
} from "@solana/spl-token";
import {
Connection,
Keypair,
SystemProgram,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
/**
* Creates the token mint with the default state
* @param connection
* @param payer
* @param mintKeypair
* @param decimals
* @param defaultState
* @returns signature of the transaction
*/
export async function createTokenExtensionMintWithDefaultState(
connection: Connection,
payer: Keypair,
mintKeypair: Keypair,
decimals: number = 2,
defaultState: AccountState,
): Promise<string> {
const mintLen = getMintLen([ExtensionType.DefaultAccountState]);
// Minimum lamports required for Mint Account
const lamports = await connection.getMinimumBalanceForRentExemption(mintLen);
const createAccountInstruction = SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: mintKeypair.publicKey,
space: mintLen,
lamports,
programId: TOKEN_2022_PROGRAM_ID,
});
const initializeDefaultAccountStateInstruction =
createInitializeDefaultAccountStateInstruction(
mintKeypair.publicKey,
defaultState,
TOKEN_2022_PROGRAM_ID,
);
const initializeMintInstruction = createInitializeMintInstruction(
mintKeypair.publicKey,
decimals,
payer.publicKey, // Designated Mint Authority
payer.publicKey, // Designated Freeze Authority
TOKEN_2022_PROGRAM_ID,
);
const transaction = new Transaction().add(
createAccountInstruction,
initializeDefaultAccountStateInstruction,
initializeMintInstruction,
);
return await sendAndConfirmTransaction(connection, transaction, [
payer,
mintKeypair,
]);
}
6. Test Setup #
Now that we have the ability to create a mint with a default state for all of it's new token accounts, let's write some tests to see how it functions.
6.1 Create Mint with Default State #
Let's first create a mint with the default state of frozen
. To do this we call
the createTokenExtensionMintWithDefaultState
function we just created in out
index.ts
file:
// CREATE MINT WITH DEFAULT STATE
await createTokenExtensionMintWithDefaultState(
connection,
payer,
mintKeypair,
decimals,
defaultState,
);
6.2 Create Test Token Accounts #
Now, let's create two 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: ourTokenAccountKeypair
and
otherTokenAccountKeypair
.
// CREATE TEST TOKEN ACCOUNTS
// Transferring from account
await createAccount(
connection,
payer,
mint,
payer.publicKey,
ourTokenAccountKeypair,
undefined,
TOKEN_2022_PROGRAM_ID,
);
// Transferring to account
await createAccount(
connection,
payer,
mint,
payer.publicKey,
otherTokenAccountKeypair,
undefined,
TOKEN_2022_PROGRAM_ID,
);
7 Tests #
Now let's write some tests to show the interactions that can be had with the
default state
extension.
We'll write four tests in total:
- Minting without thawing the recipient's account
- Minting with thawing the recipient's account
- Transferring without thawing the recipient's account
- Transferring with thawing the recipient's account
7.1 Minting without thawing the recipient's account #
This test will attempt to mint a token to ourTokenAccount
without thawing the
account. This test is expected to fail as the account will be frozen on the mint
attempt. Remember: when a token account is frozen, the balance cannot change.
To do this, let's wrap a mintTo
function in a try catch
and print out the
respected result:
// TEST: MINT WITHOUT THAWING
try {
// Attempt to mint without thawing
await mintTo(
connection,
payer,
mint,
ourTokenAccount,
payer.publicKey,
amountToMint,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
console.error("Should not have minted...");
} catch (error) {
console.log(
"✅ - We expected this to fail because the account is still frozen.",
);
}
Test this by running the script:
esrun src/index.ts
We should see the following error logged out in the terminal, meaning the
extension is working as intended.
✅ - We expected this to fail because the account is still frozen.
7.2 Minting with thawing the recipient's account #
This test will attempt to mint a token after thawing the token account. This test is expected to pass as the account will be thawed on the mint attempt.
We can create this test by calling thawAccount
and then mintTo
:
// TEST: MINT WITH THAWING
// Unfreeze frozen token
await thawAccount(
connection,
payer,
ourTokenAccount,
mint,
payer.publicKey,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
// Mint tokens to tokenAccount
await mintTo(
connection,
payer,
mint,
ourTokenAccount,
payer.publicKey,
amountToMint,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
const ourTokenAccountWithTokens = await getAccount(
connection,
ourTokenAccount,
undefined,
TOKEN_2022_PROGRAM_ID,
);
console.log(
`✅ - The new account balance is ${Number(ourTokenAccountWithTokens.amount)} after thawing and minting.`,
);
Go ahead and run the script, the transaction should succeed.
esrun src/index.ts
7.3 Transferring without thawing the recipient's account #
Now that we've tested minting, we can test transferring our tokens frozen and
not. First lets test a transfer without thawing the recipient's token account.
Remember, by default, the otherTokenAccountKeypair
is frozen due to the
extension.
Again, we expect this test to fail, since the otherTokenAccountKeypair
is
frozen and it's balance cannot change.
To test this, let's wrap a transfer
function in a try catch
:
// TEST: TRANSFER WITHOUT THAWING
try {
await transfer(
connection,
payer,
ourTokenAccount,
otherTokenAccount,
payer,
amountToTransfer,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
console.error("Should not have minted...");
} catch (error) {
console.log(
"✅ - We expected this to fail because the account is still frozen.",
);
}
Run the test and see the results:
esrun src/index.ts
7.4 Transferring with thawing the recipient's account #
The last test we'll create tests transferring tokens after thawing the token account we will be transferring to. This test is expected to pass, since all token accounts will now be thawed.
We'll do this by calling thawAccount
and then transfer
:
// TEST: TRANSFER WITH THAWING
// Unfreeze frozen token
await thawAccount(
connection,
payer,
otherTokenAccount,
mint,
payer.publicKey,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
await transfer(
connection,
payer,
ourTokenAccount,
otherTokenAccount,
payer,
amountToTransfer,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
const otherTokenAccountWithTokens = await getAccount(
connection,
otherTokenAccount,
undefined,
TOKEN_2022_PROGRAM_ID,
);
console.log(
`✅ - The new account balance is ${Number(
otherTokenAccountWithTokens.amount,
)} after thawing and transferring.`,
);
Run all of the tests one last time and see the results:
esrun src/index.ts
Remember the key takeaways:
- The
default state
extension, enforces the default state on all new token accounts. - Frozen account's balance cannot change.
Congratulations! We've just created and tested a mint using the default account extension!
Challenge #
Add tests for burning tokens from frozen and thawed token accounts (hint, one will fail, one will succeed).
To get you started:
// TEST: Burn tokens in frozen account
await freezeAccount(
connection,
payer,
ourTokenAccount,
mint,
payer.publicKey,
[],
undefined,
TOKEN_2022_PROGRAM_ID,
);
await burn(
connection,
payer,
ourTokenAccount,
mint,
payer.publicKey,
1,
[],
undefined,
TOKEN_2022_PROGRAM_ID,
);