Transfer Fee Extension
Create a token that allows a fee to be charged each time the token is traded.
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 Nexis Native Chain 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:
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
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
The third instruction createInitializeMintInstruction
initializes the mint.
Lastly, you need to add all of these instructions to a transaction and send it off the the blockchain.
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 Nexis Native Chain programming, we always specify amounts to be transferred,
minted or burned in their smallest unit. To send one NZT 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:
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
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 Nexis Native Chain 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)
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
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
:
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.
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:
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
:
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.
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.
Alternatively, if you’d like to use testnet or devnet, import the
clusterApiUrl
from @nexis-network/web3.js
and pass it to the connection as such:
If you decide to use devnet, and have issues with airdropping NZT. 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.nexis.network 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
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.
Run the script to make sure it’s working so far.
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.
Let’s run the script again, we should have a zero balance.
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.
If you’d like, run the script to check that everything is working:
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 Nexis Native Chain programming, we always specify amounts to be transferred, minted or
burned in their smallest unit. To send one NZT 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.
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:
Go ahead and run the script:
You should get the following:
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:
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:
Go ahead and run the script:
You should get the following:
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:
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:
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
:
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:
Now, let’s run it.
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.