Breaking down how fungible and non-fungible tokens work in Solana.
Understanding Solana tokens can be quite difficult if you come from an Ethereum background. In Ethereum, fungible tokens use the ERC20 standard, and non-fungible tokens use the ERC721 standard. Each ERC20 token has its own smart contract, and each NFT collection has its own ERC721 (or ERC1155) contract. Pretty simple stuff.
Things work quite differently in Solana land. Let’s get into it.
- An account either holds data (e.g. how many tokens you have) or is an executable program (i.e. a smart contract). I refer to the former as “data accounts” and the latter as “program accounts.” Importantly, unlike Ethereum, program accounts do not store state. All state is stored in data accounts.
- Each account contains the following fields 👇
- Each account has a unique address (similar to Ethereum). Most addresses are the public key of a keypair.
- Each account is owned by a program. By default, a newly created account is owned by a built-in program called the “System Program.” Only the owner of an account can modify it. For more details, you can consult the wiki I mentioned above, or read a short primer on accounts that I wrote.
Solana’s token program enables the following (for fungible and non-fungible tokens):
- Token minting
- Token transferring
- Token burning
Basically, instead of deploying a new ERC20 smart contract for each new token, all you need to do is send an instruction to the token program. Depending on what instruction you send, the token program will mint/transfer/burn tokens.
If you don’t completely understand yet, don’t worry! Going through an example should make things much clearer, which is what we’ll do next.
Note: sometimes you’ll see Solana tokens referred to as “SPL tokens.” SPL stands for Solana Program Library, which is a set of Solana programs that the Solana team has deployed on-chain. SPL tokens are similar to ERC20 tokens, since every SPL token has a standard set of functionality.
The easiest way to understand this is by going through a few examples. Our examples will cover fungible tokens, and we’ll use the spl-token command-line tool to interact with the token program (you can install it by running cargo install spl-token-cli).
First, make sure you’ve installed the Solana CLI and spl-token.
Next, run solana-keygen new -o ~/my_solana_wallet1.json, and create a new environment variable called $SOLADDR1 that stores the resulting public key. Repeat this process, but with a second environment variable called $SOLADDR2 (and name the keypair file my_solana_wallet2.json). We’ll use these addresses and keypair files later on to test out token minting and transferring.
Here’s what these values look like for me:
$ echo $SOLADDR1 3sdsSwWWjjGA7HpPBQfGaXRE2HqmdKicMXHRapqLAu4L $ echo $SOLADDR2 ES2C1YPzNh5JjQu7DdxrveaPUHj9CnrRWSdrFo4ku5Zh
Note: many of the commands in this tutorial can be made shorter by running solana config set — keypair ~/my_solana_wallet1.json, which sets the default client keypair. For example, this lets you leave off the --owner flag for many of the following commands. I use the longer version of commands to make things clearer and more explicit.
Now that we’re all set up, we can use spl-token. First, let’s create a new type of token.
$ spl-token create-token --mint-authority ~/my_solana_wallet1.json
Creating a new type of token creates a new data account, which from now on we’ll refer to as a “mint account.” Each type of token is associated with exactly one mint account. The address of the mint account is
6ifRGEkJ6XmEjuqfTFNqAjjomUiDJTjRv4GHqBH6usWr, but we’ll use
$TOKEN1 instead to make things more readable. We can query for the account’s information as follows (don’t worry about all the gobbledygook at the end):
$ solana account $TOKEN1 Public Key: Aqf1rBKNQYgX1mjE64STwV3miEwXEe2ioZzD7n4vkpXk Balance: 0.0014616 SOL Owner: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA Executable: false Rent Epoch: 207 Length: 82 (0x52) bytes 0000: 01 00 00 00 2a b0 1a 64 bb c5 f0 df bf 57 d5 61 ....*..d.....W.a 0010: 56 a8 b8 85 8f a8 0b 09 f1 f1 a2 dc 4d 51 b3 63 V...........MQ.c 0020: 8f 72 bd e9 00 00 00 00 00 00 00 00 09 01 00 00 .r.............. 0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0050: 00 00
We can check out the owner (TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA) using Solana Explorer 👇. The owner is the token program (a program account), which is responsible for creating new tokens, minting tokens, and transferring tokens.
Now let’s look at the mint account’s data, which is the stuff that comes after “Length: 82 (0x52) bytes” as shown above. It’s kind of hard to read plain hex data, so we can instead use the Solana Explorer:
Solana Explorer decodes the data and displays it in a human-readable format. Here’s what each of the fields means:
- Address—I hope this is self-explanatory 😛 This is just the mint account’s address.
- Current Supply—The number of tokens that have been minted. Since we just created the token, it is 0.
- Mint Authority—The public key of the keypair that is allowed to mint tokens (we specified this with the --mint-authority flag. If anyone else tries to mint tokens, it will fail.
- Decimals—This dictates the smallest denomination of the token. For NFTs, it should be zero. Nine is the default.
Before we move on, here’s a simple diagram showing the accounts that are in play and how they’re related. The legend is stolen from paulx’s great blog:
- “Internal Solana relations” refers to the owner field that is set on every account, e.g. the owner that’s displayed when you run solana account $TOKEN1.
- “User-space relations” refers to when a relation between two accounts is encoded in an account’s data, e.g. the “mint authority” field we saw above.
Before we can mint some tokens, we must first create a token account.
$ spl-token create-account $TOKEN1 --owner $SOLADDR1 Creating account 9EYnoqiBQmJPR55db44cF4wkN1PD5D6vjxEz61r2Ujak
From now on, we’ll use
$TOKENACCT1 instead of
9EYnoqiBQmJPR55db44cF4wkN1PD5D6vjxEz61r2Ujak, and we’ll refer to this account as a “token account.” You might be wondering—what’s a token account? Basically, it just stores how many tokens a particular user has, for a particular type of token. For example, if you own 10 of token1 and 5 of token2, you will have two token accounts.
Here’s what the token account looks like.
$ solana account $TOKENACCT1 Public Key: 9EYnoqiBQmJPR55db44cF4wkN1PD5D6vjxEz61r2Ujak Balance: 0.00203928 SOL Owner: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA Executable: false Rent Epoch: 207
The token account has relations with three other accounts. Its internal owner is the token program, since the token program must be able to modify the account (e.g. add more tokens to it). Then, in user-space, its owner is
$SOLADDR1 and its “mint,” or associated mint account, is
Here’s how all the accounts are related now:
Finally, we can mint some tokens! This part is simpler—we just specify the mint account (which determines the “type” of token to mint), the amount of tokens to mint, and the account to give them to.
$ spl-token balance $TOKEN1 --owner $SOLADDR1 0 $ spl-token mint $TOKEN1 10 $TOKENACCT1 --mint-authority ~/my_solana_wallet1.json Minting 10 tokens Token: Aqf1rBKNQYgX1mjE64STwV3miEwXEe2ioZzD7n4vkpXk Recipient: 9EYnoqiBQmJPR55db44cF4wkN1PD5D6vjxEz61r2Ujak $ spl-token balance $TOKEN1 --owner $SOLADDR1 10
In addition to using spl-token balance, you can also use spl-token accounts to view how many of each token a certain account owns.
$ spl-token accounts --owner $SOLADDR1 Token Balance --------------------------------------------------------------- Aqf1rBKNQYgX1mjE64STwV3miEwXEe2ioZzD7n4vkpXk 10
What happens if you try to mint tokens with a different value for --mint-authority?
$ spl-token mint $TOKEN1 10 $TOKENACCT1 --mint-authority ~/my_solana_wallet2.json Minting 10 tokens Token: Aqf1rBKNQYgX1mjE64STwV3miEwXEe2ioZzD7n4vkpXk Recipient: 9EYnoqiBQmJPR55db44cF4wkN1PD5D6vjxEz61r2Ujak RPC response error -32002: Transaction simulation failed: Error processing Instruction 0: custom program error: 0x4 [5 log messages]
The answer? It doesn’t work! This is because the “mint authority” field of the token account is set to a different account, and only that account is allowed to mint new tokens.
Let’s try to transfer some tokens to $SOLADDR2 (which should have been created in the “Prerequisites” section).
$ spl-token transfer $TOKEN1 1 $SOLADDR2 --owner ~/my_solana_wallet1.json
Transfer 5 tokens
Recipient associated token account: FFednTgQRKDbGYjXrXk8SWPfzSJW3Q8ApN5mGCpGdAtE
Error: Recipient's associated token account does not exist. Add
--fund-recipient to fund their account
Uh oh, it failed 😨 The error message tells us why—we’re not allowed to transfer tokens unless the recipient has a token account.
There are two ways to fix this:
- Create a token account for the recipient and then transfer the tokens.
- Use the --fund-recipient flag. This makes the sender pay for creating the recipient’s token account. We’ll go with approach #1.
$ spl-token create-account $TOKEN1 --owner $SOLADDR2 Creating account FFednTgQRKDbGYjXrXk8SWPfzSJW3Q8ApN5mGCpGdAtE $ spl-token transfer $TOKEN1 1 $SOLADDR2 --owner ~/my_solana_wallet1.json Transfer 1 tokens Sender: 9EYnoqiBQmJPR55db44cF4wkN1PD5D6vjxEz61r2Ujak Recipient: ES2C1YPzNh5JjQu7DdxrveaPUHj9CnrRWSdrFo4ku5Zh Recipient associated token account: FFednTgQRKDbGYjXrXk8SWPfzSJW3Q8ApN5mGCpGdAtE $ spl-token balance $TOKEN1 --owner $SOLADDR2 1
Nice, it worked! Let’s take a look at all the different accounts in play.
Just like how ETH can be wrapped in an ERC20 token to form wETH, SOL can be wrapped in an SPL token.
$ spl-token wrap 1 ~/my_solana_wallet1.json Wrapping 1 SOL into DXWzvX3RtZbito65G8ZgqmEzJM6Z6Fqy7NHkbpEmCUZD
Of course, you can unwrap the resulting SPL token to get back your SOL.
$ spl-token unwrap DXWzvX3RtZbito65G8ZgqmEzJM6Z6Fqy7NHkbpEmCUZD ~/my_solana_wallet1.json Unwrapping DXWzvX3RtZbito65G8ZgqmEzJM6Z6Fqy7NHkbpEmCUZD Amount: 1 SOL Recipient: 3sdsSwWWjjGA7HpPBQfGaXRE2HqmdKicMXHRapqLAu4L
Wrapping SOL like this allows SOL to be easily exchanged for other SPL tokens.
Now that we’ve walked through how fungible tokens work, understanding non-fungible tokens should be easy. I’ll just let you read the docs, since I wouldn’t add much anyways. The main important points are:
Use --decimals 0 when creating the token, since there should only be one of them.
After minting one token, disable future minting. This ensures there will only ever be one token.
In practice, no one creates NFTs this way. Instead, most people use Candy Machine, which is a tool for uploading images and metadata and minting them as NFTs. I’ll explain how that works in a future post!
That’s it! By now, you should understand how SPL tokens work and how they differ from ERC20 tokens. To recap:
- A single program called the Token Program is able to create new token types, mint new tokens, and transfer tokens between accounts.
- Unlike ERC20 tokens, where each new type of token is associated with a new ERC20 smart contract, there is only one Token Program. However, each new type of token is associated with a new mint account.
- If a user wants to own some tokens of a particular type, they need to have a token account. In other words, if someone owns five different types of tokens, they will have five different token accounts.
- You can wrap SOL in an SPL token, which allows it to be easily exchanged for other SPL tokens.
- A non-fungible SPL token is a token with zero decimals that has minted one copy and disabled future minting. If you want to see more of this kind of content, feel free to follow me on Twitter!