Summary #
- Creators can set an interest rate and store it directly on the mint account.
- The underlying token quantity for interest bearing tokens remains unchanged.
- The accrued interest can be displayed for UI purposes without the need to frequently rebase or update to adjust for accrued interest.
- The lab demonstrates configuring a mint account that is set to mint with an interest rate. The test case also shows how to update the interest rate, along with retrieving the rate from the token.
Overview #
Tokens with values that either increase or decrease over time have practical applications in the real world, with bonds being a prime example. Previously, the ability to reflect this dynamic in tokens was limited to the use of proxy contracts, necessitating frequent rebasing or updates.
The interest bearing token
extension helps with this. By leveraging the
interest bearing token
extension and the amount_to_ui_amount
function, users
can apply an interest rate to their tokens and retrieve the updated total,
including interest, at any given moment.
The calculation of interest is done continuously, factoring in the network's timestamp. However, discrepancies in the network's time could result in accrued interest being slightly less than anticipated, though this situation is uncommon.
It's important to note that this mechanism does not generate new tokens and the displayed amount simply includes the accumulated interest, making the change purely aesthetic. That being said, this is a value stored on within the mint account and programs can take advantage of this to create functionality beyond pure aesthetics.
Adding interest rate to token #
Initializing an interest bearing token 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
SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: mint,
space: mintLen,
lamports: mintLamports,
programId: TOKEN_2022_PROGRAM_ID,
}),
The second
instruction createInitializeInterestBearingMintInstruction
initializes the
interest bearing token extension. The defining argument that dictates the
interest rate will be a variable we create named rate
. The rate
is defined
in basis points.
createInitializeInterestBearingMintInstruction(
mint,
rateAuthority.publicKey,
rate,
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 interest bearing token is created with the specified rate configuration.
Fetching accumulated interest #
To retrieve the accumulated interest on a token at any given point, first use
the getAccount
function to fetch token information, including the amount and
any associated data, passing in the connection, payer's token account, and the
relevant program ID, TOKEN_2022_PROGRAM_ID
.
Next, utilize the amountToUiAmount
function with the obtained token
information, along with additional parameters such as connection, payer, and
mint, to convert the token amount to its corresponding UI amount, which
inherently includes any accumulated interest.
const tokenInfo = await getAccount(
connection,
payerTokenAccount,
undefined,
TOKEN_2022_PROGRAM_ID,
);
/**
* Get the amount as a string using mint-prescribed decimals
*
* @param connection Connection to use
* @param payer Payer of the transaction fees
* @param mint Mint for the account
* @param amount Amount of tokens to be converted to Ui Amount
* @param programId SPL Token program account
*
* @return Ui Amount generated
*/
const uiAmount = await amountToUiAmount(
connection,
payer,
mint,
tokenInfo.amount,
TOKEN_2022_PROGRAM_ID,
);
console.log("UI Amount: ", uiAmount);
The return value of uiAmount
is a string representation of the UI amount and
will look similar to this: 0.0000005000001557528245
.
Update rate authority #
Solana provides a helper function, setAuthority
, to set a new authority on an
interest bearing token.
Use the setAuthority
function to assign a new authority to the account. You'll
need to provide the connection
, the account paying for transaction fees
(payer), the token account to update (mint), the current authority's public key,
the type of authority to update (in this case, 7 represents the InterestRate
authority type), and the new authority's public key.
After setting the new authority, use the updateRateInterestBearingMint
function to update the interest rate for the account. Pass in the necessary
parameters: connection
, payer
, mint
, the new authority's public key, the
updated interest rate, and the program ID.
/**
* 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,
rateAuthority,
AuthorityType.InterestRate, // Rate type (InterestRate)
otherAccount.publicKey, // new rate authority,
[],
undefined,
TOKEN_2022_PROGRAM_ID,
);
await updateRateInterestBearingMint(
connection,
payer,
mint,
otherAccount, // new rate authority
10, // updated rate
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
Lab #
In this lab, we're establishing Interest Bearing Tokens via the Token-2022 program on Solana. We'll initialize these tokens with a specific interest rate, update the rate with proper authorization, and observe how interest accumulates on tokens over time.
1. Setup Environment #
To get started, create an empty directory named interest-bearing-token
and
navigate to it. Run npm init -y
to initialize a brand new project.
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, PublicKey } from "@solana/web3.js";
import {
ExtensionType,
getMintLen,
TOKEN_2022_PROGRAM_ID,
getMint,
getInterestBearingMintConfigState,
updateRateInterestBearingMint,
amountToUiAmount,
mintTo,
createAssociatedTokenAccount,
getAccount,
AuthorityType,
} from "@solana/spl-token";
import { initializeKeypair, makeKeypairs } from "@solana-developers/helpers";
const connection = new Connection("http://127.0.0.1:8899", "confirmed");
const payer = await initializeKeypair(connection);
const [otherAccount, mintKeypair] = makeKeypairs(2);
const mint = mintKeypair.publicKey;
const rateAuthority = payer;
const rate = 32_767;
// Create an interest-bearing token
// Create an associated token account
// Create the getInterestBearingMint function
// Attempt to update the interest rate
// Attempt to update the interest rate with the incorrect owner
// Log the accrued interest
// Log the interest-bearing mint configuration state
// Update the rate authority and attempt to update the interest rate with the new authority
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.
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");
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 1 testnet 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 theinterest bearing token
extensionotherAccount
: The account we will use to attempt to update interestotherTokenAccountKeypair
: Another token used for testing
4. Create Mint with interest bearing token #
This function is where we'll be creating the token such that all new tokens will
be created with an interest rate. Create a new file inside of src
named
token-helper.ts
.
import {
TOKEN_2022_PROGRAM_ID,
createInitializeInterestBearingMintInstruction,
createInitializeMintInstruction,
} from "@solana/spl-token";
import {
sendAndConfirmTransaction,
Connection,
Keypair,
Transaction,
PublicKey,
SystemProgram,
} from "@solana/web3.js";
export async function createTokenWithInterestRateExtension(
connection: Connection,
payer: Keypair,
mint: PublicKey,
mintLen: number,
rateAuthority: Keypair,
rate: number,
mintKeypair: Keypair,
) {
const mintAuthority = payer;
const decimals = 9;
}
This function will take the following arguments:
connection
: The connection objectpayer
: Payer for the transactionmint
: Public key for the new mintrateAuthority
: Keypair of the account that can modify the token, in this case, it ispayer
rate
: Chosen interest rate for the token. In our case, this will be32_767
, or 32767, the max rate for the interest bearing token extensionmintKeypair
: Keypair for the new mint
When creating an interest bearing token, we must create the account instruction,
add the interest instruction and initialize the mint itself. Inside of
createTokenWithInterestRateExtension
in src/token-helper.ts
there are a few
variables already created that will be used to create the interest bearing
token. Add the following code beneath the declared variables:
const extensions = [ExtensionType.InterestBearingConfig];
const mintLen = getMintLen(extensions);
const mintLamports =
await connection.getMinimumBalanceForRentExemption(mintLen);
const mintTransaction = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: mint,
space: mintLen,
lamports: mintLamports,
programId: TOKEN_2022_PROGRAM_ID,
}),
createInitializeInterestBearingMintInstruction(
mint,
rateAuthority.publicKey,
rate,
TOKEN_2022_PROGRAM_ID,
),
createInitializeMintInstruction(
mint,
decimals,
mintAuthority.publicKey,
null,
TOKEN_2022_PROGRAM_ID,
),
);
await sendAndConfirmTransaction(
connection,
mintTransaction,
[payer, mintKeypair],
undefined,
);
That's it for the token creation! Now we can move on and start adding tests.
5. Establish required accounts #
Inside of src/index.ts
, the starting code already has some values related to
the creation of the interest bearing token.
Underneath the existing rate
variable, add the following function call to
createTokenWithInterestRateExtension
to create the interest bearing token.
We'll also need to create an associated token account which we'll be using to
mint the interest bearing tokens to and also run some tests to check if the
accrued interest increases as expected.
const rate = 32_767;
// Create interest bearing token
await createTokenWithInterestRateExtension(
connection,
payer,
mint,
rateAuthority,
rate,
mintKeypair,
);
// Create associated token account
const payerTokenAccount = await createAssociatedTokenAccount(
connection,
payer,
mint,
payer.publicKey,
undefined,
TOKEN_2022_PROGRAM_ID,
);
6. Tests #
Before we start writing any tests, it would be helpful for us to have a function
that takes in the mint
and returns the current interest rate of that
particular token.
Let's utilize the getInterestBearingMintConfigState
helper provided by the SPL
library to do just that. We'll then create a function that is used in our tests
to log out the current interest rate of the mint.
The return value of this function is an object with the following values:
rateAuthority
: Keypair of the account that can modify the tokeninitializationTimestamp
: Timestamp of interest bearing token initializationpreUpdateAverageRate
: Last rate before updatelastUpdateTimestamp
: Timestamp of last updatecurrentRate
: Current interest rate
Add the following types and function:
// Create getInterestBearingMint function
interface GetInterestBearingMint {
connection: Connection;
mint: PublicKey;
}
async function getInterestBearingMint(inputs: GetInterestBearingMint) {
const { connection, mint } = inputs;
// retrieves information of the mint
const mintAccount = await getMint(
connection,
mint,
undefined,
TOKEN_2022_PROGRAM_ID,
);
// retrieves the interest state of mint
const interestBearingMintConfig =
await getInterestBearingMintConfigState(mintAccount);
// returns the current interest rate
return interestBearingMintConfig?.currentRate;
}
Updating interest rate
The Solana SPL library provides a helper function for updating the interest rate
of a token named updateRateInterestBearingMint
. For this function to work
correctly, the rateAuthority
of that token must be the same one of which the
token was created. If the rateAuthority
is incorrect, updating the token will
result in a failure.
Let's create a test to update the rate with the correct authority. Add the following function calls:
// Attempt to update interest rate
const initialRate = await getInterestBearingMint({ connection, mint });
try {
await updateRateInterestBearingMint(
connection,
payer,
mint,
payer,
0, // updated rate
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
const newRate = await getInterestBearingMint({ connection, mint });
console.log(
`✅ - We expected this to pass because the rate has been updated. Old rate: ${initialRate}. New rate: ${newRate}`,
);
} catch (error) {
console.error("You should be able to update the interest.");
}
Run npx esrun src/index.ts
. We should see the following error logged out in
the terminal, meaning the extension is working as intended and the interest rate
has been updated:
✅ - We expected this to pass because the rate has been updated. Old rate: 32767. New rate: 0
Updating interest rate with incorrect rate authority
In this next test, let's try and update the interest rate with the incorrect
rateAuthority
. Earlier we created a keypair named otherAccount
. This will be
what we use as the otherAccount
to attempt the change the interest rate.
Below the previous test we created add the following code:
// Attempt to update the interest rate as the account other than the rate authority.
try {
await updateRateInterestBearingMint(
connection,
otherAccount,
mint,
otherAccount, // incorrect authority
0, // updated rate
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
console.log("You should be able to update the interest.");
} catch (error) {
console.error(
`✅ - We expected this to fail because the owner is incorrect.`,
);
}
Now run npx esrun src/index.ts
. This is expected to fail and log out
✅ - We expected this to fail because the owner is incorrect.
Mint tokens and read interest rate
So we’ve tested updating the interest rate. How do we check that the accrued
interest increases when an account mints more tokens? We can use the
amountToUiAmount
and getAccount
helpers from the SPL library to help us
achieve this.
Let's create a for loop that 5 times and mints 100 tokens per loop and logs out the new accrued interest:
// Log accrued interest
{
// Logs out interest on token
for (let i = 0; i < 5; i++) {
const rate = await getInterestBearingMint({ connection, mint });
await mintTo(
connection,
payer,
mint,
payerTokenAccount,
payer,
100,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
const tokenInfo = await getAccount(
connection,
payerTokenAccount,
undefined,
TOKEN_2022_PROGRAM_ID,
);
// Convert amount to UI amount with accrued interest
const uiAmount = await amountToUiAmount(
connection,
payer,
mint,
tokenInfo.amount,
TOKEN_2022_PROGRAM_ID,
);
console.log(
`Amount with accrued interest at ${rate}: ${tokenInfo.amount} tokens = ${uiAmount}`,
);
}
}
You should see something similar to the logs below:
Amount with accrued interest at 32767: 100 tokens = 0.0000001000000207670422
Amount with accrued interest at 32767: 200 tokens = 0.0000002000000623011298
Amount with accrued interest at 32767: 300 tokens = 0.0000003000001246022661
Amount with accrued interest at 32767: 400 tokens = 0.00000040000020767045426
Amount with accrued interest at 32767: 500 tokens = 0.0000005000003634233328
As you can see, the interest rate increases as more tokens are minted!
Log mint config
If for some reason you need to retrieve the mint config state, we can utilize
the getInterestBearingMintConfigState
function we created earlier to display
information about the interest bearing mint state.
// Log interest bearing mint config state
const mintAccount = await getMint(
connection,
mint,
undefined,
TOKEN_2022_PROGRAM_ID,
);
// Get Interest Config for Mint Account
const interestBearingMintConfig =
await getInterestBearingMintConfigState(mintAccount);
console.log(
"\nMint Config:",
JSON.stringify(interestBearingMintConfig, null, 2),
);
This should log out something that looks similar to this:
Mint Config: {
"rateAuthority": "Ezv2bZZFTQEznBgTDmaPPwFCg7uNA5KCvMGBNvJvUmS",
"initializationTimestamp": 1709422265,
"preUpdateAverageRate": 32767,
"lastUpdateTimestamp": 1709422267,
"currentRate": 0
}
Update rate authority and interest rate #
Before we conclude this lab, let's set a new rate authority on the interest
bearing token and attempt to update the interest rate. We do this by using the
setAuthority
function and passing in the original authority, specifying the
rate type (in this case it is 7 for InterestRate
) and passing the new
authority's public key.
Once we set the new authority, we can attempt to update the interest rate.
// Update rate authority and attempt to update interest rate with new authority
try {
await setAuthority(
connection,
payer,
mint,
rateAuthority,
AuthorityType.InterestRate, // Rate type (InterestRate)
otherAccount.publicKey, // new rate authority,
[],
undefined,
TOKEN_2022_PROGRAM_ID,
);
await updateRateInterestBearingMint(
connection,
payer,
mint,
otherAccount, // new authority
10, // updated rate
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
const newRate = await getInterestBearingMint({ connection, mint });
console.log(
`✅ - We expected this to pass because the rate can be updated with the new authority. New rate: ${newRate}`,
);
} catch (error) {
console.error(
`You should be able to update the interest with new rate authority.`,
);
}
This is expected to work and the new interest rate should be 10.
Thats it! We’ve just created an interest bearing token, updated the interest rate and logged the updated state of the token!
Challenge #
Create your own interest bearing token.