Understanding The eosio.token Contract

in #eos6 years ago (edited)

I must admit that learning eosio has been no walk in the park and I can see why many people say that it has a steep learning curve. As the eosio software continues to undergo heavy rapid development there is limited documentation and few working examples to refer to. I have gotten stumped quite a few times and would like to help improve the experience for the next developer that comes along. In this article, I will go over the ‘eosio.token’ contract by breaking it down into separate parts.

What Is The eosio.token Contract?

The eosio.token contract allows for the creation of many different tokens. This gives anyone the ability to create and transfer a token. Every token must be issued by an issuer account. Since accounts can include multiple parties you can use a normal account with owner and active permissions or a custom configured account to create and manage a token. Each token is an asset type and looks like the following:

1000000000.0000 SYS
1.0000 SYMBOL
0.10 SYS

The asset type is a number (that if I recall correctly can go up to 18 decimal places) and a symbol that can be between 1–7 uppercase letters. This contract has three actions that you can use to interact with it. They are: create, issue, and transfer.

Create is used to define the characteristics of a new token. This includes the token (asset) symbol, the maximum supply, and the account allowed to issue the token. Create will also persist the new token configuration onto the blockchain. This means that the storage for the new token configuration must be staked by someone. As you will see later on, the account this contract gets deployed on (in our case ‘eosio.token’) will also pay for the tokens configuration storage.

Issue is used to increase the active supply of the token. Tokens can be continually issued until the maximum supply is reached. The issuer defined during the token creation must approve this action in order for it to succeed.

Transfer lets one account transfer tokens to another.

Deploying The Contract

The first thing you should know is that every eosio smart contract belongs to a single eosio account. A contract is basically an object that other accounts can interact with. Contracts contain “actions” that executes code on the blockchain. Contracts have direct access to store, remove, and update data on the blockchain.

Pushing an action to a contract requires the authorization of at least one account. Further accounts and permissions can be required depending on contract complexity. An account can be made of a single, or many, individuals set up in a permission based configuration. Smart contracts can only be run by a single account, and a single account can only have a single smart contract. It is best practice to give both the account and contract the same (lowercase) name.

Before you can interact with the eosio.token contract you will need to make an account with the same name and deploy the contract onto that account.

Start off by creating an account
$cleos create account eosio eosio.token <OWNER-KEY> <ACTIVE-KEY>

Then compile the contract
$cd eos/contract/eosio.token
$eosiocpp -o eosio.token.wast eosio.token.cpp

Finally deploy the contract onto the account
$cleos set contract eosio.token ../eosio.token

You can verify the contract was deployed with
$cleos get code eosio.token

Contract Architecture

The contract is broken up into two files ‘eosio.token.cpp’ and ‘eosio.token.hpp’. The ‘.hpp’ file defines the contract class, actions and tables, while the ‘.cpp’ file implements the action logic. Let’s first look at the contract class which will be used to instantiate the contract object. (I have removed some left-over code from ‘eosio.token.hpp’)

/**
 *  @file
 *  @copyright defined in eos/LICENSE.txt
 */
#pragma once

#include <eosiolib/asset.hpp>
#include <eosiolib/eosio.hpp>

#include <string>

namespace eosiosystem {
   class system_contract;
}

namespace eosio {

   using std::string;

   class token : public contract {
      public:
         token( account_name self ):contract(self){}

         void create( account_name issuer,
                      asset        maximum_supply);

         void issue( account_name to, asset quantity, string memo );

         void transfer( account_name from,
                        account_name to,
                        asset        quantity,
                        string       memo );

      private:

         friend eosiosystem::system_contract;

         inline asset get_supply( symbol_name sym )const;

         inline asset get_balance( account_name owner, symbol_name sym )const;

         struct account {
            asset    balance;

            uint64_t primary_key()const { return balance.symbol.name(); }
         };

         struct currency_stats {
            asset          supply;
            asset          max_supply;
            account_name   issuer;

            uint64_t primary_key()const { return supply.symbol.name(); }
         };

         typedef eosio::multi_index<N(accounts), account> accounts;
         typedef eosio::multi_index<N(stat), currency_stats> stats;

         void sub_balance( account_name owner, asset value );
         void add_balance( account_name owner, asset value, account_name ram_payer );

   };

   asset token::get_supply( symbol_name sym )const
   {
      stats statstable( _self, sym );
      const auto& st = statstable.get( sym );
      return st.supply;
   }

   asset token::get_balance( account_name owner, symbol_name sym )const
   {
      accounts accountstable( _self, owner );
      const auto& ac = accountstable.get( sym );
      return ac.balance;
   }

} /// namespace eosio

The constructor and actions are defined as public member functions. The constructor takes an account name (which will be the account that the contract is deployed on, aka eosio.token) and sets it to the contract variable. Note that this class is inheriting from the ‘eosio::contract’.

The tables and helper functions are provided as private members. Two inline functions are defined at the bottom yet never used. This leaves us with the important functions sub_balance() and add_balance(). These will get called by the transfer action.

Tables

The two tables defined are accounts and stat. The accounts table is made up of different account objects each holding the balance for a different token. The stat table is made up of currency_stats objects (defined by struct currency_stats) that holds a supply, a max_supply, and an issuer. Before continuing on, it is important to know that this contract will hold data into two different scopes. The accounts table is scoped to an eosio account, and the stat table is scoped to a token symbol name.

According to the ‘eosio::multi_index’ definitions, code is the name of the account which has write permission and the scope is the account where the data gets stored.

The scope is essentially a way to compartmentalize data in a contract so that it is only accessible within a defined space. In the token contract, each eosio account is used as the scope of the accounts table. The accounts table is a multi-index container which holds multiple account objects. Each account object is indexed by its token symbol and contains the token balance. When you query a users’ accounts table using their scope, a list of all the tokens that user has an existing balance for is returned.

Here is how I visualize it.
accounts-scope.jpg
In the above picture, there is an eosio account named tom who has his own scope. Within his scope is a table called accounts. Within that table is a separate account object for each token he holds, ‘SYS’ and ‘EOS’.

There is also a second table called stat. This table will contain the status for an existing token. New tokens get created in their own symbol name scope. Within the scope is a stat table that holds a currency_stats object. Unlike the accounts table, which contains many different account objects, the stat table only holds a single currency_stats object for the given token symbol.
currency-stats-scope.jpg

Actions

Actions are implemented in the ‘.cpp’ file. In order to create a new token, the create action must be sent. Create takes two parameters; the issuer, and the maximum supply for the new token. The issuer is the only one allowed to increase the new token supply. The issuer cannot issue more than the maximum supply.

void token::create( account_name issuer,
                    asset        maximum_supply )
{

The first line of code simply requires the authorization of the contract account itself. This can be given with the command line flag -p eosio.token when pushing the action.

    require_auth( _self );

The next few lines extract the symbol for the maximum_supply asset passed in and performs some error handling. If any of the eosio_assert’s fail, then all the code will be rolled back and the transaction will not be pushed onto the blockchain.

    auto sym = maximum_supply.symbol;
    eosio_assert( sym.is_valid(), "invalid symbol name" );
    eosio_assert( maximum_supply.is_valid(), "invalid supply");
    eosio_assert( maximum_supply.amount > 0, "max-supply must be positive");

A stat table called statstable is constructed using the symbols name (token symbol) as its scope. The code checks to see if the token already exists. If it does not, then it creates and saves the new token status onto the blockchain. The first parameter _self in the emplace function means that this contracts account ‘eosio.token’ will pay for the staked storage.

    stats statstable( _self, sym.name() );
    auto existing = statstable.find( sym.name() );
    eosio_assert( existing == statstable.end(), "token with symbol already exists" );

    statstable.emplace( _self, [&]( auto& s ) {
       s.supply.symbol = maximum_supply.symbol;
       s.max_supply    = maximum_supply;
       s.issuer        = issuer;
    });
}

Note that the supply's symbol is saved since it is used as the key for locating the table row, but the supplies amount has not been issued yet.

You can now perform the next action, issue. Issue will take the account who will receive the issued tokens, the token quantity being issued, and a memo. The issue action performs two actions in one because it will both modify the created tokens supply and call the transfer action to send the issued tokens. Again, the first few lines extract the token symbol and perform error checking.

void token::issue( account_name to, asset quantity, string memo )
{
    auto sym = quantity.symbol;
    eosio_assert( sym.is_valid(), "invalid symbol name" );
    eosio_assert( memo.size() <= 256, "memo has more than 256 bytes" );

The following code section will construct the stat table using the symbol name as the scope. This is used as the key to find the token that was created previously with the create action.

    auto sym_name = sym.name();
    stats statstable( _self, sym_name );
    auto existing = statstable.find( sym_name );
    eosio_assert( existing != statstable.end(), "token with symbol does not exist, create token before issue" );
    const auto& st = *existing;

Note that the existing currency_stat returned from statstable.find() is an iterator which points to the found object. For brevity, a new variable called st is declared and set to the actual object that the existing iterator is pointing to. This lets us use the . operator to access the member variables instead of pointer notation ->.

The issuer for the created token is required to sign this transaction, and more error handling is performed.

    require_auth( st.issuer );
    eosio_assert( quantity.is_valid(), "invalid quantity" );
    eosio_assert( quantity.amount > 0, "must issue positive quantity" );

    eosio_assert( quantity.symbol == st.supply.symbol, "symbol precision mismatch" );
    eosio_assert( quantity.amount <= st.max_supply.amount - st.supply.amount, "quantity exceeds available supply");

Finally, the currency_stats st for our existing token is modified and the issued quantity is added to the supply. The issuer will also have the supply added to their balance so that the initial supply can be traced back to their account.

    statstable.modify( st, 0, [&]( auto& s ) {
       s.supply += quantity;
    });

    add_balance( st.issuer, quantity, st.issuer );

    if( to != st.issuer ) {
       SEND_INLINE_ACTION( *this, transfer, {st.issuer,N(active)}, {st.issuer, to, quantity, memo} );
    }
}

Immediately after, the transfer function is called via the SEND_INLINE_ACTION() macro which will transfer the funds. The arguments are as follows:

  1. *this - the contract code the action belongs to
  2. transfer - the anme of the action
  3. {st.issuer, N(active)} - the permissions required for the action
  4. {st.issuer, to, quantity, memo} - the arguments for the action itself

This brings us to our final action transfer. Transfer will take four input arguments from, to, quantity, and memo. from is whom will be sending the tokens, thus the quantity will be subtracted from their balance. to is whom will be receiving the tokens and thus the quantity will be added to their balance. quantity is the amount of tokens being sent, and memo is a string that you can send along with the transaction. The memo is not used or stored within this contract.

The action starts off by requiring the from accounts permission and performing error handling on the from and to accounts. The symbol is extracted from the quantity and used to get the currency_stats for the token.

void token::transfer( account_name from,
                      account_name to,
                      asset        quantity,
                      string       memo )
{
    eosio_assert( from != to, "cannot transfer to self" );
    require_auth( from );
    eosio_assert( is_account( to ), "to account does not exist");
    auto sym = quantity.symbol.name();
    stats statstable( _self, sym );
    const auto& st = statstable.get( sym );

The require_recipient() function will notify both the sender and receiver upon action completion.

    require_recipient( from );
    require_recipient( to );

More error handling is done and finally the two private functions sub_balance() and add_balance() are called to subtract the token balance from the sender and increase the token balance for the receiver.

Here is the full ‘eosio.token.cpp’ file

/**
 *  @file
 *  @copyright defined in eos/LICENSE.txt
 */

#include "eosio.token.hpp"

namespace eosio {

void token::create( account_name issuer,
                    asset        maximum_supply )
{
    require_auth( _self );

    auto sym = maximum_supply.symbol;
    eosio_assert( sym.is_valid(), "invalid symbol name" );
    eosio_assert( maximum_supply.is_valid(), "invalid supply");
    eosio_assert( maximum_supply.amount > 0, "max-supply must be positive");

    stats statstable( _self, sym.name() );
    auto existing = statstable.find( sym.name() );
    eosio_assert( existing == statstable.end(), "token with symbol already exists" );

    statstable.emplace( _self, [&]( auto& s ) {
       s.supply.symbol = maximum_supply.symbol;
       s.max_supply    = maximum_supply;
       s.issuer        = issuer;
    });
}


void token::issue( account_name to, asset quantity, string memo )
{
    auto sym = quantity.symbol;
    eosio_assert( sym.is_valid(), "invalid symbol name" );
    eosio_assert( memo.size() <= 256, "memo has more than 256 bytes" );

    auto sym_name = sym.name();
    stats statstable( _self, sym_name );
    auto existing = statstable.find( sym_name );
    eosio_assert( existing != statstable.end(), "token with symbol does not exist, create token before issue" );
    const auto& st = *existing;

    require_auth( st.issuer );
    eosio_assert( quantity.is_valid(), "invalid quantity" );
    eosio_assert( quantity.amount > 0, "must issue positive quantity" );

    eosio_assert( quantity.symbol == st.supply.symbol, "symbol precision mismatch" );
    eosio_assert( quantity.amount <= st.max_supply.amount - st.supply.amount, "quantity exceeds available supply");

    statstable.modify( st, 0, [&]( auto& s ) {
       s.supply += quantity;
    });

    add_balance( st.issuer, quantity, st.issuer );

    if( to != st.issuer ) {
       SEND_INLINE_ACTION( *this, transfer, {st.issuer,N(active)}, {st.issuer, to, quantity, memo} );
    }
}

void token::transfer( account_name from,
                      account_name to,
                      asset        quantity,
                      string       memo )
{
    eosio_assert( from != to, "cannot transfer to self" );
    require_auth( from );
    eosio_assert( is_account( to ), "to account does not exist");
    auto sym = quantity.symbol.name();
    stats statstable( _self, sym );
    const auto& st = statstable.get( sym );

    require_recipient( from );
    require_recipient( to );

    eosio_assert( quantity.is_valid(), "invalid quantity" );
    eosio_assert( quantity.amount > 0, "must transfer positive quantity" );
    eosio_assert( quantity.symbol == st.supply.symbol, "symbol precision mismatch" );
    eosio_assert( memo.size() <= 256, "memo has more than 256 bytes" );


    sub_balance( from, quantity );
    add_balance( to, quantity, from );
}

void token::sub_balance( account_name owner, asset value ) {
   accounts from_acnts( _self, owner );

   const auto& from = from_acnts.get( value.symbol.name(), "no balance object found" );
   eosio_assert( from.balance.amount >= value.amount, "overdrawn balance" );


   if( from.balance.amount == value.amount ) {
      from_acnts.erase( from );
   } else {
      from_acnts.modify( from, owner, [&]( auto& a ) {
          a.balance -= value;
      });
   }
}

void token::add_balance( account_name owner, asset value, account_name ram_payer )
{
   accounts to_acnts( _self, owner );
   auto to = to_acnts.find( value.symbol.name() );
   if( to == to_acnts.end() ) {
      to_acnts.emplace( ram_payer, [&]( auto& a ){
        a.balance = value;
      });
   } else {
      to_acnts.modify( to, 0, [&]( auto& a ) {
        a.balance += value;
      });
   }
}

} /// namespace eosio

EOSIO_ABI( eosio::token, (create)(issue)(transfer) )

Example commands:

$cleos push action eosio.token create '["usera","21000000.0000 DEMO"]' -p eosio.token usera 

$cleos push action eosio.token issue '["usera","21000000.0000 DEMO","issuance"]' -p usera 

$cleos push action eosio.token tranfser '["usera","userb","1000000.0000 DEMO","here you go"]' -p usera 

Table commands:

$cleos get table eosio.token DEMO stat 
{ 
    "rows": [{ 
        "supply": "21000000.0000 DEMO" 
        "max_supply": "2100000000.0000 DEMO" 
        "issuer": "usera" 
      } 
    ], 
    "more": false 
} 

$cleos get table eosio.token usera accounts 
{ 
    "rows": [{ 
        "balance": "20000000.0000 DEMO" 
      } 
    ], 
    "more": false 
} 

 
$cleos get table eosio.token userb accounts 
{ 
    "rows": [{ 
        "balance": "10000000.0000 DEMO" 
      } 
    ], 
    "more": false 
} 



Note: This article was written at the time of the Dawn4.1 code release

Sort:  

I hope the EOS tokens end up being valuable. The disclaimers make no promises whatsoever. We'll find out in the near future once the platform goes live.

Do not be concerned about the disclaimer, that is just for regulation purposes. It is also no different than any other token when it comes to value, they are worth what we are willing to pay.

EOS is so much than just a price, holders can rent tokens, participate in the RAM trading market and will will get numerous dApp airdrops that will one day be worth more than EOS combined.

EOS is a game changer for the entire crypto industry, we do not have long let now until we go live.

Great job here!

Yes ! this is the type of content needed. Was looking exactly for this a few days back.

@jd3 well done i love the article...no wonder dan upvoted your post

Thanks a lot! I can't believe Dan saw it :)

Very cool stuff!

Hello, My name is Jarib Figueredo and I'm the Founder of HorusPay. We are a Decentralized Global Payroll Platform Daap. We would like to get in contact with you. Could you please reach out to us? Our website is www.horuspay.io and our Telegram group is https://t.me/horuspayUS

Just saw this, Jarib. Looks like this turned into a wonderful relationship :) Seems like ages ago. Go Horuspay!

I can run issue and transfer commands from https://github.com/EOSIO/eos/wiki/Tutorial-eosio-token-Contract

How do I run get_supply and get_balance?

I'm trying commands like
cleos push action eosio.asset get_balance '[ "rayadmin", "SYS" ]' -p rayadmin

but no luck so far.

get_supply and get_balance are not actions that you can use with cleos. Only the actions defined in the macro EOSIO_ABI( eosio::token, (create)(issue)(transfer) ) at the bottom of eosio.token.cpp can be used as actions with cleos. get_supply and get_balance are not exposed directly as actions, instead, I believe you would call them from inside other functions that you would write on your own. You can get the balance for rayadmin by doing

cleos get table eosio.asset rayadmin accounts

and the supply with

cleos get table eosio.asset SYS stat

I want to do this!

Excellent post, Jack!

in the Tables section you state: "According to the ‘eosio::multi_index’ definitions, code is the name of the account which has write permission and the scope is the account where the data gets stored." but neither "code" nor "scope" are actually seen in the typedefs, so this is not readily comprehensible. could you clarify?