Create a token that allows a fee to be charged each time the token is traded.
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 the withdrawWithheldAuthority
authoritytransfer fee
extension need to use the
transferCheckedWithFee
instructiontransfer 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:
transfer fee
extension involves three
instructions:
SystemProgram.createAccount
createInitializeTransferFeeConfigInstruction
createInitializeMintInstruction
SystemProgram.createAccount
allocates space on the
blockchain for the mint account. This instruction accomplishes three things:
space
lamports
for rentgetMintLen
and
getMinimumBalanceForRentExemption
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 accountcreateInitializeMintInstruction
initializes the mint.
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:
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 youwithdrawWithheldAuthority
can withdraw directly from the withheld
portion of a user’s token account into any “token vault”withdrawWithheldAuthority
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.
getProgramAccounts
withdrawWithheldTokensFromAccounts
function (the authority
needs
to be a signer)withdrawWithheldAuthority
can withdraw the tokens from the
mint at any point.
To harvest:
harvestWithheldTokensToMint
withdrawWithheldTokensFromMint
solana config
wallet is
the transferFeeConfigAuthority
:
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.
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:
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.
initializeKeypair
with airdropping, follow the
next step.
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.
clusterApiUrl
from @nexis-network/web3.js
and pass it to the connection as such:
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.
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 feesrc/index.ts
. We’ll create a
mint that has nine decimal points, 1000 fee basis points (10%), and a max fee
of 5000.
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.
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.
calculateFee
helper function. We did it manually for demonstration purposes.
The following is one way to accomplish this: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
.
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 if owner
is a multisigsources
: Source accounts from which to withdraw withheld feesconfirmOptions
: Options for confirming the transactionprogramId
: SPL Token program account - in our case TOKEN_2022_PROGRAM_ID
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: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:
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 accountgetMint
and
then read the transfer fee
extension data on it by calling
getTransferFeeConfig
:
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 if owner
is a multisigconfirmOptions
: Options for confirming the transactionprogramId
: SPL Token program accountsolution
branch of
this repository.