This tutorial provides explanations and a step-by-step procedure to create and deploy an EOSIO blockchain token starting with the sample EOSIO token smart contract. It is recommended, for understanding the sample EOSIO token smart contract, to read the EOSIO Token Tutorial.
Introduction
An EOSIO-based blockchain allows you to create tokens. There are many ways in which you can create tokens. One way is to leverage the existing eosio.token
sample smart contract and build on top of it a token that meets your requirements.
Learning Objectives
The learning objectives for this tutorial are:
- Create a new token starting with the
eosio.token
sample smart contract - Customize the
eosio.token
sample smart contract - Build and deploy a smart contract to a blockchain that uses EOSIO tokens
- Create, issue, and transfer EOSIO tokens
- Create and use the airgrab action
Create A New Token
To create a new token, create the smart contract that manages that token. A token can take many forms and have various features.
Select of the following techniques to create a smart contract that manages a token:
- Develop the smart contract from scratch
- Use the existing ‘eosio.token’ sample smart contract as is
- Use the existing ‘eosio.token’ sample smart contract and customize it to meet your requirements
This tutorial demonstrates the steps to customize, build and deploy a new token smart contract, which manages a token with symbol NEWT, starting with the eosio.token
sample smart contract.
Procedure
This procedure shows you how to implement the following five steps to extend or change the ‘eosio.token’ smart contract:
- Duplicate the original
eosio.token
smart contract - Specialize the new token according to its requirements
- Extend the new token functionality with an
airgrab
action - Compile and deploy the new token
- Thoroughly test your token to behave correctly in all cases
Prerequisites
Make sure the following prerequisites are met before starting this procedure.
Nodeos and Wallet Prerequisites
- There is a local nodeos started with the default connection port. Refer to instructions on how to start one in this how to.
- Your public and private key pair are in your wallet and the wallet is opened before executing the commands presented in the following sections.
Test Accounts Prerequisites
Make sure you have test accounts ready in your development environment for testing purposes. The tutorial uses newt and ama accounts. You can create them, or use the ones you already have and substitute them accordingly.
You can learn here how to create accounts with the cleos command line tool. Or you can execute the following commands and replace the public key with your own:
cleos create account eosio newt EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos create account eosio ama EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
New Token Smart Contract Requirements
- Your smart contract creates and manages only one token with symbol NEWT
- The NEWT token can be created only by the smart contract account owner
- The maximum amount of NEWT tokens is 21 million
- The NEWT token can be airgrabed by a particular set of accounts. Only the account names that start with a vowel will be able to grab your token, with a first come first served rule
Note: Whenever you find the EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV public key is part of the shell commands, you will have to substitute it with your own public key.
Step 1: Duplicate The eosio.token Sources
Clone the eosio.token repo from GitHub and copy its sources to a new folder newt.
cd ~
git clone https://github.com/EOSIO/eosio.token.git
mkdir newt
cp eosio.token/contracts/eosio.token/include/eosio.token/eosio.token.hpp newt
cp eosio.token/contracts/eosio.token/src/eosio.token.cpp newt
cd newt
Rename the files to suit the newt
token name.
mv eosio.token.hpp newt.hpp
mv eosio.token.cpp newt.cpp
Change this line from newt.hpp
class [[eosio::contract("eosio.token")]] token : public contract {
to
class [[eosio::contract("newt")]] token : public contract {
This change tells the compiler that the customized smart contract name is newt
. The attribute [[eosio::contract]]
, if it has no parameter specified, uses by default the contract token
class name. The EOSIO framework uses the name of the contract to logically connect actions, tables and notification handlers to a specific compiled smart contract. The contract name if specified in the [[eosio::contract]]
attribute must meet the restrictions of an EOSIO account name whereas the contract class name is free of those constraints.
Change this line from newt.cpp
#include <eosio.token/eosio.token.hpp>
to
#include "newt.hpp"
Build And Deploy The newt Smart Contract
As a quick verification step build and deployed your new token to the blockchain.
To build the newt token run the following commands from command shell:
cd ~/newt
eosio-cpp -abigen -abigen_output=newt.abi -o newt.wasm newt.cpp
ls -al
If you executed every step correctly and no error occurs the last command prints all the files in the current directory and the list of files looks like this:
. .. newt.abi newt.cpp newt.hpp newt.wasm
You can now deploy the newt.wasm on the blockchain. To do so run the following command:
cleos set contract newt . --abi newt.abi newt.wasm -p newt@active
Step 2. Specialize The newt Smart Contract
The eosio.token
smart contract is generic and it can deal with multiple tokens created by different accounts. Therefore the newly created token you have at this time has the exact same properties as the eosio.token
. However, one of the requirements for your token is to be specialized, that is, to deal with only one token and thus only one account token owner. To realize this restriction you have to make the following changes.
Update The create Action
Update the create
action with the code below:
void token::create() {
require_auth(get_self());
auto sym = symbol("NEWT", 4); // NEWT is the token symbol with precision 4
auto maximum_supply = asset(210000000000, sym);
stats statstable(get_self(), sym.code().raw());
auto existing = statstable.find(sym.code().raw());
check(existing == statstable.end(), "token with symbol already created");
statstable.emplace(get_self(), [&](auto &s) {
s.supply.symbol = sym;
s.max_supply = maximum_supply;
s.issuer = get_self();
});
}
Note: The create action now does not need input parameters, because the only token that can be created is the NEWT token and can only be created by the smart contract owner.
Also the create
action mints 21 million tokens. Note how the 21 million is specified in the code as 210000000000
, that is 21 million followed by 4 zeros. Because the precision was defined one line above as 4, you have to add 4 zeros at the end of the 21 million.
Update The issue Action
Update the issue
action with the code below:
void token::issue(const asset &quantity, const string &memo) {
require_auth(get_self());
auto sym = quantity.symbol;
auto newtsym_code = symbol("NEWT", 4); // NEWT is the token symbol with precision 4
check(sym.code() == newtsym_code.code(), "This contract can handle NEWT tokens only.");
check(sym.is_valid(), "invalid symbol name");
check(memo.size() <= 256, "memo has more than 256 bytes");
stats statstable(get_self(), sym.code().raw());
auto existing = statstable.find(sym.code().raw());
check(existing != statstable.end(), "token with symbol does not exist, create token before issue");
const auto& existing_token = *existing;
require_auth( existing_token.issuer );
check(quantity.is_valid(), "invalid quantity");
check(quantity.amount > 0, "must issue positive quantity");
check(quantity.symbol == existing_token.supply.symbol, "symbol precision mismatch");
check(quantity.amount <= existing_token.max_supply.amount - existing_token.supply.amount,
"quantity exceeds available supply");
statstable.modify(existing_token, same_payer, [&](auto &s) {
s.supply += quantity;
});
add_balance(existing_token.issuer, quantity, existing_token.issuer);
}
The issue
action does not need the to
parameter anymore because the issuing of new tokens can only be done for NEWT token symbol and for only one account, the smart contract owner account.
There are also two new lines of code introduced which check if the asset
received as parameter matches the NEWT token symbol.
auto newtsym_code = symbol("NEWT", 4); // NEWT is the token symbol with precision 4
check(sym.code() == newtsym_code.code(), "This contract can handle NEWT tokens only.");
Update The retire Action
Update the retire
action with the code below:
void token::retire(const asset &quantity, const string &memo) {
auto sym = quantity.symbol;
check(sym.is_valid(), "invalid symbol name");
check(memo.size() <= 256, "memo has more than 256 bytes");
auto newtsym_code = symbol("NEWT", 4); // NEWT is the token symbol with precision 4
check(sym.code() == newtsym_code.code(), "This contract can handle NEWT tokens only.");
stats statstable(get_self(), sym.code().raw());
auto existing = statstable.find(sym.code().raw());
check(existing != statstable.end(), "token with symbol does not exist");
const auto &st = *existing;
require_auth(st.issuer);
check(quantity.is_valid(), "invalid quantity");
check(quantity.amount > 0, "must retire positive quantity");
check(quantity.symbol == st.supply.symbol, "symbol precision mismatch");
statstable.modify(st, same_payer, [&](auto &s) {
s.supply -= quantity;
});
sub_balance(st.issuer, quantity);
}
The only addition to this action is the check to make sure that the asset
received as input parameter matches the NEWT token symbol.
auto newtsym_code = symbol("NEWT", 4); // NEWT is the token symbol with precision 4
check(sym.code() == newtsym_code.code(), "This contract can handle NEWT tokens only.");
Update The transfer Action
Update the transfer
action with the code below:
void token::transfer(const name &from,
const name &to,
const asset &quantity,
const string &memo) {
check(from != to, "cannot transfer to self");
require_auth(from);
check(is_account(to), "to account does not exist");
auto sym = quantity.symbol.code();
auto newtsym_code = symbol("NEWT", 4); // NEWT is the token symbol with precision 4
check(sym == newtsym_code.code(), "This contract can handle NEWT tokens only.");
stats statstable(get_self(), sym.raw());
const auto &st = statstable.get(sym.raw());
require_recipient(from);
require_recipient(to);
check(quantity.is_valid(), "invalid quantity");
check(quantity.amount > 0, "must transfer positive quantity");
check(quantity.symbol == st.supply.symbol, "symbol precision mismatch");
check(memo.size() <= 256, "memo has more than 256 bytes");
auto payer = has_auth(to) ? to : from;
sub_balance(from, quantity);
add_balance(to, quantity, payer);
}
As with the previous action, the only addition to this action is the check to make sure that the asset
received as input parameter matches the NEWT token symbol.
auto newtsym_code = symbol("NEWT", 4); // NEWT is the token symbol with precision 4
check(sym.code() == newtsym_code.code(), "This contract can handle NEWT tokens only.");
Step 3. Extend The newt Smart Contract
Another requirement for your token is to be able to be airgrabbed by accounts which start with a vowel. To realize this, extend the newt
smart contract with an airgrab
action. The airgrab
action allows an account which starts with a vowel to receive 100 NEWT tokens only once.
In newt.hpp, right after the close
action, add a declaration for the airgrab
action like below:
[[eosio::action]]
void airgrab(const name &owner);
Create an action wrapper next to the already existing ones:
using airgrab_action = eosio::action_wrapper<"airgrab"_n, &token::airgrab>;
Finally, create a new table that stores all accounts that airgrabbed tokens. Include the following structure and type definition:
struct [[eosio::table]] airgrab_list {
name account;
uint64_t primary_key()const { return account.value; }
};
typedef eosio::multi_index< "airgrabs"_n, airgrab_list > airgrabs;
Add in the private methods declaration section the starts_with_vowel
which checks if an account name starts with a vowel:
int starts_with_vowel(string account_name);
Move to newt.cpp and add the airgrab
action and starts_with_vowel
implementations at the end of the contract:
void token::airgrab(const name &owner) {
check(starts_with_vowel(owner.to_string()) == 0,
"Account is not qualified, it must start with a vowel.");
check(_self != owner, "Cannot airgrab from NEWT owner account.");
require_auth(owner);
require_recipient(_self); // from
require_recipient(owner); // to
auto sym = symbol("NEWT", 4); // NEWT is the token symbol with precision 4
asset airgrabbed_asset(1000000, sym); // allow 100 tokens to be airgrabbed
// Check if the user have airgrabbed their tokens
airgrabs airgrab_table(get_self(), sym.raw());
auto it = airgrab_table.find(owner.value);
check(it == airgrab_table.end(), "You have already airgrabbed your tokens");
sub_balance(_self, airgrabbed_asset);
add_balance(owner, airgrabbed_asset, owner);
// Register the airgrab so it will not be able to do it the second time
airgrab_table.emplace(owner, [&](auto &row) {
row.account = owner;
});
}
int token::starts_with_vowel(string account_name) {
if (!(account_name[0] == 'A' || account_name[0] == 'a' || account_name[0] == 'E'
|| account_name[0] == 'e' || account_name[0] == 'I' || account_name[0] == 'i'
|| account_name[0] == 'O' || account_name[0] == 'o' || account_name[0] == 'U'
|| account_name[0] == 'u'))
return 1;
else
return 0;
}
Go through the airgrab action implementation. You should be familiar with most of the checks. This section will focus on the new parts of the code.
The first check is for the account name to start with a vowel.
Then it checks for the account which does the airgrab to not be the smart contract owner.
In the next lines of code, you initialize the table that you will use to keep track of all accounts that claimed their free tokens. You also have a check that prevents accounts from claiming tokens twice.
Finally, if the account airgrabs the tokens for the first time, the code updates the account’s balance, transfers the tokens and updates the airgrab table. The same steps are done by the transfer
action as well. The difference this time is that the account which receives the airgrabbed tokens pays for the used resources. Note the owner
account which is sent as payer (last parameter) to the add_balance
method invocation:
add_balance(owner, airgrabbed_asset, owner);
Step 4: Compile And Deploy NEWT Token To The Blockchain
Compile and deploy your newly created token smart contract to the blockchain.
Compile
Compile the smart contract:
eosio-cpp -abigen -abigen_output=newt.abi -o newt.wasm newt.cpp
Deploy
Deploy the smart contract with the newt account:
cleos set contract newt . --abi newt.abi newt.wasm -p newt@active
Step 5: Test NEWT Token
To test the NEWT token execute the following actions using the cleos
command line tool.
Create a new token
To create the NEWT token use the following command:
cleos push action newt create '[]' -p newt@active
Output:
executed transaction: 098fb8b4089d86f36acf21c47a6f62946a5af162c14ad6a876e7aba06a7c5505 96 bytes 233 us
# newt <= newt::create ""
warning: transaction executed locally, but may not be confirmed by the network yet ]
Try to execute the same command again. You will see it failed with the below error message:
Error 3050003: eosio_assert_message assertion failure
Error Details:
assertion failure with message: token with symbol already created
pending console output:
Execute the following command to verify that newt
account owns the NEWT tokens:
cleos get table newt NEWT stat
Output:
{
"rows": [{
"supply": "0.0000 NEWT",
"max_supply": "21000000.0000 NEWT",
"issuer": "newt"
}
],
"more": false,
"next_key": "",
"next_key_bytes": ""
}
A similar command to the one above is the following:
cleos get currency stats newt NEWT
Issue New Tokens
Issue 1000 NEWT tokens. To execute the action, use the following command:
cleos push action newt issue '["1000.0000 NEWT", "issue 1000 NEWT"]' -p newt@active
Output:
executed transaction: 56e205345a6cf397b8ba0684f375996daff5e2865f424bd0261c4747b740c755 128 bytes 176 us
# newt <= newt::issue {"quantity":"1000.0000 NEWT","memo":"issue 1000 NEWT"}
warning: transaction executed locally, but may not be confirmed by the network yet ]
You can check again with the same command from above that the supply for the NEWT token changed:
cleos get table newt NEWT stat
Output:
{
"rows": [{
"supply": "1000.0000 NEWT",
"max_supply": "21000000.0000 NEWT",
"issuer": "newt"
}
],
"more": false,
"next_key": "",
"next_key_bytes": ""
}
Transfer Tokens
Now that you have issued tokens, you can transfer NEWT tokens to the ama
test account you created earlier in the tutorial. To transfer 100 NEWT tokens use the following command:
cleos push action newt transfer '[ "newt", "ama", "100.0000 NEWT", "enjoy the NEWT tokens, spend them wisely" ]' -p newt@active
Output:
executed transaction: 6fd8c626658ca4aaca1f255ef31b58bd3de507055be440c6214391ae34737ea6 168 bytes 333 us
# newt <= newt::transfer {"from":"newt","to":"ama","quantity":"100.0000 NEWT","memo":"enjoy the NEWT tokens, spend them wisel...
# ama <= newt::transfer {"from":"newt","to":"ama","quantity":"100.0000 NEWT","memo":"enjoy the NEWT tokens, spend them wisel...
warning: transaction executed locally, but may not be confirmed by the network yet ]
A similar command to the one above is the following:
cleos transfer newt ama "100.0000 NEWT" "enjoy the NEWT tokens, spend them wisely" -c newt -p newt
Check the balances have updated on the two accounts:
cleos get currency balance newt newt NEWT
Output:
900.0000 NEWT
cleos get currency balance newt ama NEWT
Output:
100.0000 NEWT
Airgrab tokens
Finally, test the airgrab action. To execute it use the command below:
cleos push action newt airgrab '[ "ama", "100.0000 NEWT"]' -p ama@active
Output:
executed transaction: 9310694a185896c4706ce1db94f15e8139a73a921ad301919ba63498d03e36b1 104 bytes 257 us
# newt <= newt::airgrab {"owner":"ama"}
# ama <= newt::airgrab {"owner":"ama"}
warning: transaction executed locally, but may not be confirmed by the network yet ]
If you try to airgrab again for ama
account you will see the error below:
Error 3050003: eosio_assert_message assertion failure
Error Details:
assertion failure with message: You have already airgrabbed your tokens
pending console output:
You can check now the ama
account balance:
cleos get currency balance newt ama NEWT
And the output is:
200.0000 NEWT
You can also verify that the airgrab was successful if you check the airgrabs
table with the following command:
cleos get table newt NEWT airgrabs
Output:
{
"rows": [{
"account": "ama"
}
],
"more": false,
"next_key": "",
"next_key_bytes": ""
}
Summary
This tutorial demonstrated:
- How the
eosio.token
smart contract works - How to create a new token based on the
eosio.token
smart contract - How to customize your new token
- How to airgrab your new token
- How to deploy and test your new token
Acknowledgements
Thank you Dimo (@ddzhurenov) for your assistance with this tutorial.
Next Steps
If you want to learn more, check the tic-tac-toe game smart contract tutorial.