Summary #
- The Token Extension Program's
transfer fee
extension allows fees to be withheld on every transfer. These fees are held on the recipient's account, and can only be redeemed from thewithdrawWithheldAuthority
authority - Withheld tokens can be withdrawn directly from the recipient accounts or can be harvested back to the mint and then withdrawn
- Transfers with mints using the
transfer fee
extension need to use thetransferCheckedWithFee
instruction
Overview #
Suppose you're a Solana game developer and you're making a large open world
multiplayer role playing game. You'll have a currency in this game that all the
players will earn and trade with. To make the economy in the game circular, you
may want to charge a small transfer fee every time this currency changes hands,
you'd call this the developer tax. This can be accomplished with the
transfer fee
extension. The neat part is this will work on every transfer,
in-game and out!
The Token Extension Program's transfer fee
extension enables you to configure
a transfer fee on a mint such that fees are assessed at the protocol level. On
every transfer, some amount of that mint is withheld on the recipient account
which cannot be used by the recipient. At any point after the transfer, the
withdraw
authority can claim these withheld tokens.
The transfer fee
extension is customizable and updatable. Here are the inputs
that we'll delve into a bit later:
- Fee basis points: This is the fee assessed on every transfer. For example, if 1000 tokens with 50 basis points are transferred, it will yield 5 tokens.
- Maximum fee: The cap on transfer fees. With a maximum fee of 5000 tokens, a transfer of 10,000,000,000,000 tokens will only yield 5000 tokens.
- Transfer fee authority: The entity that can modify the fees.
- Withdraw withheld authority: The entity that can move tokens withheld on the mint or token accounts.
Calculating fee basis points #
Before we go into the extension, here's a quick intro to "fee basis points".
A basis point is a unit of measurement used in finance to describe the percentage change in the value or rate of a financial instrument. One basis point is equivalent to 0.01% or 0.0001 in decimal form.
To get the fee we must calculate it as follows:
Fee = (token_amount * fee_basis_points) / 10000
The constant 10,000 is used to convert the fee basis point percentage to the equivalent amount.
Configuring a mint with a transfer fee #
Initializing a mint with the transfer fee
extension 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
As with all Token Extension Program's mints, we need to calculate the space and
lamports needed for the mint. We can get these by calling getMintLen
and
getMinimumBalanceForRentExemption
const extensions = [ExtensionType.TransferFeeConfig];
const mintLength = getMintLen(extensions);
const mintLamports =
await connection.getMinimumBalanceForRentExemption(mintLength);
const createAccountInstruction = SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: mintKeypair.publicKey,
space: mintLength,
lamports: mintLamports,
programId: TOKEN_2022_PROGRAM_ID,
});
The second instruction createInitializeTransferFeeConfigInstruction
initializes the transfer fee extension.
It takes the following parameters:
mint
: Token mint accounttransferFeeConfigAuthority
: Optional authority that can update the feeswithdrawWithheldAuthority
: Optional authority that can withdraw feestransferFeeBasisPoints
: Amount of transfer collected as fees, expressed as basis points of the transfer amountmaximumFee
: Maximum fee assessed on transfersprogramId
: SPL Token program account
const initializeTransferFeeConfigInstruction =
createInitializeTransferFeeConfigInstruction(
mintKeypair.publicKey,
payer.publicKey,
payer.publicKey,
feeBasisPoints,
maxFee,
TOKEN_2022_PROGRAM_ID,
);
The third instruction createInitializeMintInstruction
initializes the mint.
const initializeMintInstruction = createInitializeMintInstruction(
mintKeypair.publicKey,
decimals,
payer.publicKey,
null,
TOKEN_2022_PROGRAM_ID,
);
Lastly, you need to add all of these instructions to a transaction and send it off the the blockchain.
const mintTransaction = new Transaction().add(
createAccountInstruction,
initializeTransferFeeConfigInstruction,
initializeMintInstruction,
);
const signature = await sendAndConfirmTransaction(
connection,
mintTransaction,
[payer, mintKeypair],
{ commitment: "finalized" },
);
Transferring mint with transfer fees #
There are a couple of notes when transferring tokens with the transfer fee
extension.
First, the recipient is the one who "pays" for the fee. If I send 100 tokens with basis points of 50 (5%), the recipient will receive 95 tokens (five withheld)
Second, the fee is calculated not by the tokens sent, but the smallest unit of
said token. In Solana programming, we always specify amounts to be transferred,
minted or burned in their smallest unit. To send one SOL to someone, we actually
send 1 * 10 ^ 9
lamports. Another way to look at it is if you wanted to send
one US dollar, you're actually sending 100 pennies. Let's make this dollar a
token with a 50 basis points (5%) transfer fee. Sending one dollar, would result
in a five cent fee. Now let's say we have a max fee of 10 cents, this will
always be the highest fee, even if we send $10,000.
The calculation can be summed up like this:
const transferAmount = BigInt(tokensToSend * 10 ** decimals);
const basisPointFee =
(transferAmount * BigInt(feeBasisPoints)) / BigInt(10_000);
const fee = basisPointFee > maxFee ? maxFee : basisPointFee;
Third and final, there are two ways to transfer tokens with the transfer fee
extension: transfer_checked
or transfer_checked_with_fee
. The regular
transfer
function lacks the necessary logic to handle fees.
You have the choice of which function to use for transferring:
transfer_checked_with_fee
: You have to calculate and provide the correct feestransfer_checked
: This will calculate the fees for you
/**
* Transfer tokens from one account to another, asserting the token mint and decimals
*
* @param connection Connection to use
* @param payer Payer of the transaction fees
* @param source Source account
* @param mint Mint for the account
* @param destination Destination account
* @param owner Owner of the source account
* @param amount Number of tokens to transfer
* @param decimals Number of decimals in transfer 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
*/
const secondTransferAmount = BigInt(1 * 10 ** decimals);
const secondTransferSignature = await transferChecked(
connection,
payer,
sourceAccount,
mint,
destinationAccount,
sourceKeypair,
secondTransferAmount,
decimals, // Can also be gotten by getting the mint account details with `getMint(...)`
[],
undefined,
TOKEN_2022_PROGRAM_ID,
);
Collecting fees #
There are two ways to "collect fees" from the withheld portion of the token accounts.
- The
withdrawWithheldAuthority
can withdraw directly from the withheld portion of a user's token account into any "token vault" - We can "harvest" the withheld tokens and store them within the mint account
itself, which can be withdrawn at any point from the
withdrawWithheldAuthority
But first, why have these two options?
Simply put, directly withdrawing is a permissioned function, meaning only the
withdrawWithheldAuthority
can call it. Whereas harvesting is permissionless,
where anyone can call the harvest function consolidating all of the fees into
the mint itself.
But why not just directly transfer the tokens to the fee collector on each transfer?
Two reasons: one, where the mint creator wants the fees to end up may change. Two, this would create a bottleneck.
Say you have a very popular token with transfer fee
enabled and your fee vault
is the recipient of the fees. If thousands of people are trying to transact the
token simultaneously, they'll all have to update your fee vault's balance - your
fee vault has to be "writable". While it's true Solana can execute in parallel,
it cannot execute in parallel if the same accounts are being written to at the
same time. So, these thousands of people would have to wait in line, slowing
down the transfer drastically. This is solved by setting aside the withheld
transfer fees within the recipient's account - this way, only the sender and
receiver's accounts are writable. Then the withdrawWithheldAuthority
can
withdraw to the fee vault anytime after.
Directly withdrawing fees #
In the first case, If we want to withdraw all withheld transfer fees from all token accounts directly we can do the following:
- Grab all token accounts associated with the mint using
getProgramAccounts
- Add all token accounts with some withheld tokens to a list
- Call the
withdrawWithheldTokensFromAccounts
function (theauthority
needs to be a signer)
// grabs all of the token accounts for a given mint
const accounts = await connection.getProgramAccounts(TOKEN_2022_PROGRAM_ID, {
commitment: "finalized",
filters: [
{
memcmp: {
offset: 0,
bytes: mint.toString(),
},
},
],
});
const accountsToWithdrawFrom = [];
for (const accountInfo of accounts) {
const unpackedAccount = unpackAccount(
accountInfo.pubkey,
accountInfo.account,
TOKEN_2022_PROGRAM_ID,
);
// If there is withheld tokens add it to our list
const transferFeeAmount = getTransferFeeAmount(unpackedAccount);
if (
transferFeeAmount != null &&
transferFeeAmount.withheldAmount > BigInt(0)
) {
accountsToWithdrawFrom.push(accountInfo.pubkey);
}
}
/**
* Withdraw withheld tokens from accounts
*
* @param connection Connection to use
* @param payer Payer of the transaction fees
* @param mint The token mint
* @param destination The destination account
* @param authority The mint's withdraw withheld tokens authority
* @param multiSigners Signing accounts if `owner` is a multisig
* @param sources Source accounts from which to withdraw withheld fees
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
*
* @return Signature of the confirmed transaction
*/
await withdrawWithheldTokensFromAccounts(
connection,
payer,
mint,
feeVaultAccount,
authority,
[],
accountsToWithdrawFrom,
{ commitment: "finalized" },
TOKEN_2022_PROGRAM_ID,
);
Harvesting fees #
The second approach we call "harvesting" - this is a permissionless function
meaning anyone can call it. This approach is great for "cranking" the harvest
instruction with tools like clockwork. The
difference is when we harvest, the withheld tokens get stored in the mint
itself. Then the withdrawWithheldAuthority
can withdraw the tokens from the
mint at any point.
To harvest:
- gather all of the accounts you want to harvest from (same flow as above)
- call
harvestWithheldTokensToMint
- To withdraw from the mint, call
withdrawWithheldTokensFromMint
/**
* Harvest withheld tokens from accounts to the mint
*
* @param connection Connection to use
* @param payer Payer of the transaction fees
* @param mint The token mint
* @param sources Source accounts from which to withdraw withheld fees
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
*
* @return Signature of the confirmed transaction
*/
await harvestWithheldTokensToMint(
connection,
payer,
mint,
accountsToHarvestFrom,
{ commitment: "finalized" },
TOKEN_2022_PROGRAM_ID,
);
/**
* Withdraw withheld tokens from mint
*
* @param connection Connection to use
* @param payer Payer of the transaction fees
* @param mint The token mint
* @param destination The destination account
* @param authority The mint's withdraw withheld tokens authority
* @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 withdrawWithheldTokensFromMint(
connection,
payer,
mint,
feeVaultAccount,
authority,
[],
{ commitment: "finalized" },
TOKEN_2022_PROGRAM_ID,
);
Updating fees #
As of right now there is no way to set the transfer fee post
creation with the JS library.
However you can from the CLI assuming the result of solana config
wallet is
the transferFeeConfigAuthority
:
solana address
## The result of ^ needs to be the `transferFeeConfigAuthority`
spl-token set-transfer-fee <MINT_ID> <FEE_IN_BASIS_POINTS> <MAX_FEE>
Updating authorities #
If you'd like to change the transferFeeConfigAuthority
or the
withdrawWithheldAuthority
you can with the setAuthority
function. Just pass
in the correct accounts and the authorityType
, which in these cases are:
TransferFeeConfig
and WithheldWithdraw
, respectively.
/**
* 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.TransferFeeConfig, // or AuthorityType.WithheldWithdraw
newAuthority,
[],
undefined,
TOKEN_2022_PROGRAM_ID,
);
Lab #
In this lab, we are going to create a transfer fee configured mint. We'll use a fee vault to hold the transfer fees, and we'll collect the fees using both the direct and the harvesting methods.
1. Getting started #
To get started, create an empty directory named transfer-fee
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 { Connection, Keypair } from "@solana/web3.js";
import { initializeKeypair } from "@solana-developers/helpers";
import { transferCheckedWithFee } from "@solana/spl-token";
/**
* Create a connection and initialize a keypair if one doesn't already exists.
* If a keypair exists, airdrop a SOL token if needed.
*/
const connection = new Connection("http://127.0.0.1:8899");
const payer = await initializeKeypair(connection);
console.log(`public key: ${payer.publicKey.toBase58()}`);
const mintKeypair = Keypair.generate();
const mint = mintKeypair.publicKey;
console.log("\nmint public key: " + mintKeypair.publicKey.toBase58() + "\n\n");
// CREATE MINT WITH TRANSFER FEE
// CREATE FEE VAULT ACCOUNT
// CREATE A SOURCE ACCOUNT AND MINT TOKEN
// CREATE DESTINATION ACCOUNT
// TRANSFER TOKENS
// FETCH ACCOUNTS WITH WITHHELD TOKENS
// WITHDRAW WITHHELD TOKENS
// VERIFY UPDATED FEE VAULT BALANCE
// HARVEST WITHHELD TOKENS TO MINT
// WITHDRAW HARVESTED TOKENS
// VERIFY UPDATED FEE VAULT BALANCE
index.ts
has a main function that creates a connection to the specified
validator node and calls initializeKeypair
. This main
function is where
we'll be writing our script.
Go ahead and run the script. You should see the mint
public key logged to your
terminal.
esrun src/index.ts
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. Create a mint with transfer fee #
Let's create a function createMintWithTransferFee
in a new file
src/create-mint.ts
.
To create a mint with the transfer fee
extension, we need three instructions:
SystemProgram.createAccount
, createInitializeTransferFeeConfigInstruction
and createInitializeMintInstruction
.
We'll also want the our new createMintWithTransferFee
function to have
following arguments:
connection
: The connection objectpayer
: Payer for the transactionmintKeypair
: Keypair for the new mintdecimals
: Mint decimalsfeeBasisPoints
: Fee basis points for the transfer feemaxFee
: Maximum fee points for the transfer fee
import {
sendAndConfirmTransaction,
Connection,
Keypair,
SystemProgram,
Transaction,
TransactionSignature,
} from "@solana/web3.js";
import {
ExtensionType,
createInitializeMintInstruction,
getMintLen,
TOKEN_2022_PROGRAM_ID,
createInitializeTransferFeeConfigInstruction,
} from "@solana/spl-token";
export async function createMintWithTransferFee(
connection: Connection,
payer: Keypair,
mintKeypair: Keypair,
decimals: number,
feeBasisPoints: number,
maxFee: bigint,
): Promise<TransactionSignature> {
const extensions = [ExtensionType.TransferFeeConfig];
const mintLength = getMintLen(extensions);
const mintLamports =
await connection.getMinimumBalanceForRentExemption(mintLength);
console.log("Creating a transaction with transfer fee instruction...");
const mintTransaction = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: mintKeypair.publicKey,
space: mintLength,
lamports: mintLamports,
programId: TOKEN_2022_PROGRAM_ID,
}),
createInitializeTransferFeeConfigInstruction(
mintKeypair.publicKey,
payer.publicKey,
payer.publicKey,
feeBasisPoints,
maxFee,
TOKEN_2022_PROGRAM_ID,
),
createInitializeMintInstruction(
mintKeypair.publicKey,
decimals,
payer.publicKey,
null,
TOKEN_2022_PROGRAM_ID,
),
);
console.log("Sending transaction...");
const signature = await sendAndConfirmTransaction(
connection,
mintTransaction,
[payer, mintKeypair],
{ commitment: "finalized" },
);
console.log("Transaction sent");
return signature;
}
Now let's import and call our new function in src/index.ts
. We'll create a
mint that has nine decimal points, 1000 fee basis points (10%), and a max fee
of 5000.
// CREATE MINT WITH TRANSFER FEE
const decimals = 9;
const feeBasisPoints = 1000;
const maxFee = BigInt(5000);
await createMintWithTransferFee(
connection,
payer,
mintKeypair,
decimals,
feeBasisPoints,
maxFee,
);
Run the script to make sure it's working so far.
esrun src/index.ts
4. Create a fee vault account #
Before we transfer any tokens and accrue transfer fees, let's create a "fee vault" that will be the final recipient of all transfer fees.
For simplicity, let's make the fee vault the associated token account (ATA) of our payer.
// CREATE FEE VAULT ACCOUNT
console.log("\nCreating a fee vault account...");
const feeVaultAccount = await createAssociatedTokenAccount(
connection,
payer,
mintKeypair.publicKey,
payer.publicKey,
{ commitment: "finalized" },
TOKEN_2022_PROGRAM_ID,
);
const initialBalance = (
await connection.getTokenAccountBalance(feeVaultAccount, "finalized")
).value.amount;
console.log("Current fee vault balance: " + initialBalance + "\n\n");
Let's run the script again, we should have a zero balance.
esrun src/index.ts
5. Create two token accounts and mint to one #
Let's now create two test token accounts we'll call the source
and
destination
accounts. Then let's mint some tokens to the source
.
We can do this by calling createAccount
and mintTo
.
We'll mint 10 full tokens.
// CREATE TEST ACCOUNTS AND MINT TOKENS
console.log("Creating source account...");
const sourceKeypair = Keypair.generate();
const sourceAccount = await createAccount(
connection,
payer,
mint,
sourceKeypair.publicKey,
undefined,
{ commitment: "finalized" },
TOKEN_2022_PROGRAM_ID,
);
console.log("Creating destination account...");
const destinationKeypair = Keypair.generate();
const destinationAccount = await createAccount(
connection,
payer,
mint,
destinationKeypair.publicKey,
undefined,
{ commitment: "finalized" },
TOKEN_2022_PROGRAM_ID,
);
console.log("Minting 10 tokens to source...\n\n");
const amountToMint = 10 * 10 ** decimals;
await mintTo(
connection,
payer,
mint,
sourceAccount,
payer,
amountToMint,
[payer],
{ commitment: "finalized" },
TOKEN_2022_PROGRAM_ID,
);
If you'd like, run the script to check that everything is working:
esrun src/index.ts
6. Transfer one token #
Now, let's transfer 1 token from our sourceAccount
to our destinationAccount
and see what happens.
To transfer a token with the transfer fee
extension enabled, we have to call
transferCheckedWithFee
. This requires us to decide how much we want to send,
and to calculate the correct fee associated.
To do this, we can do a little math:
First, to send one full token is actually sending 1 * (10 ^ decimals)
tokens.
In Solana programming, we always specify amounts to be transferred, minted or
burned in their smallest unit. To send one SOL to someone, we actually send
1 * 10 ^ 9
lamports. Another way to look at it is if you wanted to send one US
dollar, you're actually sending 100 pennies.
Now, we can take the resulting amount: 1 * (10 ^ decimals)
and calculate the
fee using the basis points. We can do this by taking the transferAmount
multiplying it by the feeBasisPoints
and dividing by 10_000
(the definition
of a fee basis point).
Lastly, we need to check if the fee is more than the max fee, if it is, then we
call transferCheckedWithFee
with our max fee.
const transferAmount = BigInt(1 * 10 ** decimals);
const basisPointFee =
(transferAmount * BigInt(feeBasisPoints)) / BigInt(10_000);
const fee = basisPointFee > maxFee ? maxFee : basisPointFee;
With all of this information, take a second, what do you think the final balances and withheld amounts for this transaction will be?
Now, let's transfer one of our tokens and print out the resulting balances:
// TRANSFER TOKENS
console.log("Transferring with fee transaction...");
const transferAmount = BigInt(1 * 10 ** decimals);
const fee = (transferAmount * BigInt(feeBasisPoints)) / BigInt(10_000);
const transferSignature = await transferCheckedWithFee(
connection,
payer,
sourceAccount,
mint,
destinationAccount,
sourceKeypair.publicKey,
transferAmount,
decimals,
fee,
[sourceKeypair],
{ commitment: "finalized" },
TOKEN_2022_PROGRAM_ID,
);
const sourceAccountAfterTransfer = await getAccount(
connection,
sourceAccount,
undefined,
TOKEN_2022_PROGRAM_ID,
);
const destinationAccountAfterTransfer = await getAccount(
connection,
destinationAccount,
undefined,
TOKEN_2022_PROGRAM_ID,
);
const withheldAmountAfterTransfer = getTransferFeeAmount(
destinationAccountAfterTransfer,
);
console.log(`Source Token Balance: ${sourceAccountAfterTransfer.amount}`);
console.log(
`Destination Token Balance: ${destinationAccountAfterTransfer.amount}`,
);
console.log(
`Withheld Transfer Fees: ${withheldAmountAfterTransfer?.withheldAmount}\n`,
);
Go ahead and run the script:
esrun src/index.ts
You should get the following:
Transferring with fee transaction...
Source Token Balance: 9000000000
Destination Token Balance: 999995000
Withheld Transfer Fees: 5000
A little breakdown:
Our fee basis points are 1000, meaning 10% of the amount transferred should be used as a fee. In this case 10% of 1,000,000,000 is 100,000,000, which is way bigger than our 5000 max fee. So that's why we see 5000 withheld. Additionally, note that the receiver is the one who "pays" for the transfer fee.
From now on, to calculate fees, you may want to use the
calculateFee
helper function. We did it manually for demonstration purposes.
The following is one way to accomplish this:
const transferAmount = BigInt(1 * 10 ** decimals);
const mintAccount = await getMint(
connection,
mint,
undefined,
TOKEN_2022_PROGRAM_ID,
);
const transferFeeAmount = getTransferFeeConfig(mintAccount);
const fee = calculateFee(
transferFeeAmount?.newerTransferFee!,
secondTransferAmount,
);
7. Withdrawing fees #
There are two ways in which we can collect fees from the recipient's account
into the fee vault. The first one is withdrawing the withheld fees directly from
the recipient's account itself to the fee vault account using
withdrawWithheldTokensFromAccounts
. The second approach is "harvesting" the
fees from the recipient's account to the mint with harvestWithheldTokensToMint
and then withdrawing it from the mint to the fee vault account with
withdrawWithheldTokensFromMint
.
7.1 Withdraw fees directly from the recipient accounts #
First, let's withdraw the fees directly. We can accomplish this by calling
withdrawWithheldTokensFromAccounts
. This is a permissioned function, meaning
only the withdrawWithheldAuthority
can sign for it.
The withdrawWithheldTokensFromAccounts
function takes the following
parameters:
connection
: The connection to usepayer
: The payer keypair of the transaction feesmint
: The token mintdestination
: The destination account - in our case, the fee vaultauthority
: The mint's withdraw withheld tokens authority - in our case, the payermultiSigners
: Signing accounts ifowner
is a multisigsources
: Source accounts from which to withdraw withheld feesconfirmOptions
: Options for confirming the transactionprogramId
: SPL Token program account - in our caseTOKEN_2022_PROGRAM_ID
Now, let's directly withdraw the fees from the destination account and check the resulting balances:
// DIRECTLY WITHDRAW
await withdrawWithheldTokensFromAccounts(
connection,
payer,
mint,
feeVaultAccount,
payer.publicKey,
[],
[destinationAccount],
undefined,
TOKEN_2022_PROGRAM_ID,
);
const withheldAccountAfterWithdraw = await getAccount(
connection,
destinationAccount,
undefined,
TOKEN_2022_PROGRAM_ID,
);
const withheldAmountAfterWithdraw = getTransferFeeAmount(
withheldAccountAfterWithdraw,
);
const feeVaultAfterWithdraw = await getAccount(
connection,
feeVaultAccount,
undefined,
TOKEN_2022_PROGRAM_ID,
);
console.log(
`Withheld amount after withdraw: ${withheldAmountAfterWithdraw?.withheldAmount}`,
);
console.log(
`Fee vault balance after withdraw: ${feeVaultAfterWithdraw.amount}\n`,
);
Go ahead and run the script:
esrun src/index.ts
You should get the following:
Withheld amount after withdraw: 0
Fee vault balance after withdraw: 5000
The withdrawWithheldTokensFromAccounts
can also be used
to collect all fees from all token accounts, if you fetch them all first.
Something like the following would work:
const accounts = await connection.getProgramAccounts(TOKEN_2022_PROGRAM_ID, {
commitment: "finalized",
filters: [
{
memcmp: {
offset: 0,
bytes: mint.toString(),
},
},
],
});
const accountsToWithdrawFrom = [];
for (const accountInfo of accounts) {
const unpackedAccount = unpackAccount(
accountInfo.pubkey,
accountInfo.account,
TOKEN_2022_PROGRAM_ID,
);
const transferFeeAmount = getTransferFeeAmount(unpackedAccount);
if (
transferFeeAmount != null &&
transferFeeAmount.withheldAmount > BigInt(0)
) {
accountsToWithdrawFrom.push(accountInfo.pubkey);
}
}
await withdrawWithheldTokensFromAccounts(
connection,
payer,
mint,
feeVaultAccount,
payer.publicKey,
[],
accountsToWithdrawFrom,
undefined,
TOKEN_2022_PROGRAM_ID,
);
7.2 Harvest and then withdraw #
Let's look at the second option to retrieving the withheld fees: "harvesting".
The difference here is that instead of withdrawing the fees directly, we
"harvest" them back to the mint itself using harvestWithheldTokensToMint
. This
is a permissionless function, meaning anyone can call it. This is useful if you
use something like clockwork to automate these
harvesting functions.
After the fees are harvested to the mint account, we can call
withdrawWithheldTokensFromMint
to transfer these tokens into our fee vault.
This function is permissioned and we need the withdrawWithheldAuthority
to
sign for it.
To do this, we need to transfer some more tokens to accrue more fees. This time,
we're going to take a shortcut and use the transferChecked
function instead.
This will automatically calculate our fees for us. Then we'll print out the
balances to see where we are at:
// TRANSFER TOKENS PT2
console.log("Transferring with fee transaction pt2...");
const secondTransferAmount = BigInt(1 * 10 ** decimals);
const secondTransferSignature = await transferChecked(
connection,
payer,
sourceAccount,
mint,
destinationAccount,
sourceKeypair,
secondTransferAmount,
decimals, // Can also be gotten by getting the mint account details with `getMint(...)`
[],
undefined,
TOKEN_2022_PROGRAM_ID,
);
const sourceAccountAfterSecondTransfer = await getAccount(
connection,
sourceAccount,
undefined,
TOKEN_2022_PROGRAM_ID,
);
const destinationAccountAfterSecondTransfer = await getAccount(
connection,
destinationAccount,
undefined,
TOKEN_2022_PROGRAM_ID,
);
const withheldAmountAfterSecondTransfer = getTransferFeeAmount(
destinationAccountAfterTransfer,
);
console.log(`Source Token Balance: ${sourceAccountAfterSecondTransfer.amount}`);
console.log(
`Destination Token Balance: ${destinationAccountAfterSecondTransfer.amount}`,
);
console.log(
`Withheld Transfer Fees: ${withheldAmountAfterSecondTransfer?.withheldAmount}\n`,
);
Now, let's harvest the tokens back to the mint account. We will do this using
the harvestWithheldTokensToMint
function. This function takes the following
parameters:
connection
: Connection to usepayer
: Payer of the transaction feesmint
: The token mintsources
: Source accounts from which to withdraw withheld feesconfirmOptions
: Options for confirming the transactionprogramId
: SPL Token program account
Then we'll check the resulting balances. However, since the withheld amount will
now be stored in the mint, we have to fetch the mint account with getMint
and
then read the transfer fee
extension data on it by calling
getTransferFeeConfig
:
// HARVEST WITHHELD TOKENS TO MINT
await harvestWithheldTokensToMint(
connection,
payer,
mint,
[destinationAccount],
undefined,
TOKEN_2022_PROGRAM_ID,
);
const withheldAccountAfterHarvest = await getAccount(
connection,
destinationAccount,
undefined,
TOKEN_2022_PROGRAM_ID,
);
const withheldAmountAfterHarvest = getTransferFeeAmount(
withheldAccountAfterHarvest,
);
const mintAccountAfterHarvest = await getMint(
connection,
mint,
undefined,
TOKEN_2022_PROGRAM_ID,
);
const mintTransferFeeConfigAfterHarvest = getTransferFeeConfig(
mintAccountAfterHarvest,
);
console.log(
`Withheld amount after harvest: ${withheldAmountAfterHarvest?.withheldAmount}`,
);
console.log(
`Mint withheld amount after harvest: ${mintTransferFeeConfigAfterHarvest?.withheldAmount}\n`,
);
Lastly, let's withdraw these fees from the mint itself using the
withdrawWithheldTokensFromMint
function. This function takes the following
parameters:
connection
: Connection to usepayer
: Payer of the transaction feesmint
: The token mintdestination
: The destination accountauthority
: The mint's withdraw withheld tokens authoritymultiSigners
: Signing accounts ifowner
is a multisigconfirmOptions
: Options for confirming the transactionprogramId
: SPL Token program account
After that, let's check the balances:
// WITHDRAW HARVESTED TOKENS
await withdrawWithheldTokensFromMint(
connection,
payer,
mint,
feeVaultAccount,
payer,
[],
undefined,
TOKEN_2022_PROGRAM_ID,
);
const mintAccountAfterSecondWithdraw = await getMint(
connection,
mint,
undefined,
TOKEN_2022_PROGRAM_ID,
);
const mintTransferFeeConfigAfterSecondWithdraw = getTransferFeeConfig(
mintAccountAfterSecondWithdraw,
);
const feeVaultAfterSecondWithdraw = await getAccount(
connection,
feeVaultAccount,
undefined,
TOKEN_2022_PROGRAM_ID,
);
console.log(
`Mint withheld balance after second withdraw: ${mintTransferFeeConfigAfterSecondWithdraw?.withheldAmount}`,
);
console.log(
`Fee Vault balance after second withdraw: ${feeVaultAfterSecondWithdraw.amount}`,
);
Now, let's run it.
esrun src/index.ts
You should see the balances after every step of the way.
That's it! We have successfully created a mint with a transfer fee. If you get
stuck at any point, you can find the working code in the solution
branch of
this repository.
Challenge #
Create a transfer fee enabled mint and transfer some tokens with different decimals, fee transfer points and max fees.