💙 Are you a DApp? We're helping DApps reach out to more users with our promotion service. Contact us now!
View all posts
2019-03-09

EOS Smart Contract Development (Part — 4): Analysing EOS standard token contract

Article written by quillhash.com and brought to you by EOS GO

Handling money in the form of digitized assets has been one of the primary applications of blockchain based dapps. Understanding how digital tokens work is essential in understanding a blockchain based platform, for it being a topic that will also come up frequently in application development. In this article, we will try to understand the handling of tokens on EOSIO via the standard eosio.token contract.

We will discuss the how native EOS tokens work, and how could we deploy our own tokens on EOS blockchain. We will also discuss the standard token contract in technical depth, which will also throw light on the nitty-gritty details of EOS smart contract development in general. It is assumed that the reader have some familiarity with modern C++, although we will try to explain each line.

EOS_SC_dev_4

Before reading this, we would recommend to read the previous articles in this series, to gain a better understanding of EOS platform and basic smart contract concepts in general. So let’s get started!

Standard token contract as a System Contract

Before coming to standard token contract first, we need to understand how the smart contracts play a vital role in most intrinsic functionalities of EOSIO. Tasks like buying and selling RAM, and delegating bandwidth are implemented as a smart contract running on the EOS blockchain. These contracts are present at the genesis of the EOSIO blockchain, so they are also referred as system contracts.

Similarly, the native currency (EOS) is handled using the eosio.token contract. So this contract is responsible for maintaining the EOS balance for every account holder. Thus when we do any EOS transfers between accounts, it is actually being done by issuing transfer action on the eosio.token contract. The administration of this contract is handled by the multi-signature authority of minimum 15 out of 21 block producers, since it is a system contract. However, we are free to use and modify its code to our needs, and deploy on our accounts to create custom tokens. Let us now move on to analyze the structure of the eosio.token contract.

Overview of the eosio.token contract structure

Blockchain developers familiar with Ethereum will tend to compare the popular ERC20 token standard to the eosio.token contract. However, both of them are quite different in their approach. The general rule of thumb in smart contract development is to make them as simple as possible, as complex design leads to potential vulnerabilities, which are to be avoided at any cost considering the decentralized nature of handling of funds. Because of this, eosio.token contract was designed to be very simple and consists of very basic functionalities. Also unlike ERC20 standard, we can define multiple currencies on a single token contract. Following actions are exposed by the eosio.token contract:

  • create : Used for creating new currency on this contract.
  • issue : Issue the created currency to other accounts. This is called by issuer of the currency.
  • retire : Decrease the maximum supply of the currency, raising currency price.
  • transfer : Transfer specified amount of currency from one account to another.
  • close : Delete the holder balance record from contract’s database if empty balance.

Persistent information like account balances and currency types are stored in RAM, and accessed via multi-index tables. As we can see, eosio.token contract exposes only bare essential features and avoids any complexities. Let’s discuss the technical details of the eosio.token contract.

Technical Analysis of eosio.token contract

EOS smart contracts are implemented as a C++ class and inherits from the eosio::contract base class. This base class consists of one data member named _self, which can also be accessed using it’s get_self method. This member represents the account where this smart contract has been deployed on. The instance of this contract is created every time any action is issued for this contract. The eosio::contract base class holds the integer representation (code) for the action currently being executed.

The header file (eosio.token.hpp) contains the member and method declarations, and the (eosio.token.cpp) file contains their definitions. The structure of eosio.token contracts start with defining the contract class and inheriting from the eosio::contract base class.

namespace eosio {

    class [[eosio::contract("eosio.token")]] token : public contract {
    public:
    // all the actions and the static definitions go here

    private:
    // private helper methods and table definitions go here
    };
}

Smart contracts are compiled to web assembly using the eos-cpp module of the eosio.cdt, which is a contract development toolkit that provides tools and libraries for creating contracts and converting them to web assembly, and generate ABI. For that to happen, we need to specify pre compilation instructions to eosio.cdt so that it can recognize types and methods for wasm (web assembly file) and ABI. ABI is a JSON file that consists of the declarations of the types and methods exposed by the smart contract, which is useful for interaction with the contract.

These pre compilation instructions are specified by using C++ attributes. In above code, we can see the statement [[eosio::contract("eosio.token")]] which specify that this class represent a contract. All the action definitions and the static methods (if defined) are public members. State variables, table structure definitions, and helper methods are private members as a convention.

Important types used in making contracts eosio::asset is a type that is used to handle tokens efficiently. It prevents numerical overflows and enables easy and secure handling of tokens. eosio::asset comprises of two fields, amount and symbol that store the amount of the token, and the symbol of the token respectively. Thus the value 100.0000 SYM can be represented as an asset with amount as 100 and symbol as ‘SYM’. Also note that symbol itself is defined by eosio::symbol_type and integer precision, and in our example, symbol_type is ‘SYM’ and precision is 4.

eosio::name is a type that is used to represent an account inside smart contract. It encapsulates a string and it’s integer representation. It also exposes an overloaded operator _n which can be used to convert a string to eosio::name type. For example we can convert string “mystring” into eosio::name like “mystring”_n.

Tables used in eosio.token contract For defining multi-index tables, we first need to define the structure of the rows that will be stored inside table, and then use that type for defining the multi-index table. Standard token contract uses two tables, account and currency_stats to store balances and information about the currencies respectively. Let’s discuss each of them.

Table: account

// defining type
    struct [[eosio::table]] account {
	    asset  balance;
	    uint64_t primary_key()const {

	    return balance.symbol.code().raw();

    }
};

// instantiating table
typedef eosio::multi_index< "accounts"_n, account > accounts;

This table is used to hold the balances for various currency types for a given account. First we have defined a struct with a field balance of type asset and a method called primary key that returns 64-bit unsigned integer. Field balance represents the amount of a given currency type.

The method primary_key() is used by the multi-index table to fetch the primary key value for the given row, which must be of the type uint64_t. Here, it is returning the raw integer equivalent for the symbol. Thus each row in this table will represent a unique currency type along with its associated balance.

Next, we define the table type by specifying the name for the table and the type it will hold. “accounts”_n represents the table name of the type eosio::name, and account refers to the struct we declared above.

We can then instantiate this table by specifying code and scope, to use them inside functions to access the persistent storage. Code refers to the name of the contract this table belongs to, and scope is used to separate the concern for the table. For example,

eosio::name bob_acc = “bob”_n;
accounts to_acnts( _self, bob_acc.value)

Here we have instantiated table called to_acnts of type accounts with code as _self and scope as integer representation of bob account. Thus we will get access to all type of tokens that are held by Bob. With scope, we can further segregate the values of the tables.

Table: stats

struct [[eosio::table]] currency_stats {
    asset  supply;
    asset  max_supply;
    name issuer;

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

typedef eosio::multi_index< "stat"_n, currency_stats > stats;

This table is used to store all the currency types that are being issued using this contract. It holds the current supply, maximum supply (both of the type eosio::asset) and the issuer account. Here the issuer is account that created the token type specified by supply and max_supply.

This table is instantiated with code as the smart contract account (_self) but with scope as the integer representation of symbol type. This means that this table is segregated on the basis of the symbol type of the tokens.

stats statstable( _self, sym.code().raw() );

Action: create

void token::create( name  issuer,
				    asset  maximum_supply )
{
    require_auth( _self ); // can only be called by this contract

    // check for invalid values
    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");

    // check for already existing symbol
    stats statstable( _self, sym.code().raw() );
    auto existing = statstable.find( sym.code().raw() );
    eosio_assert( existing == statstable.end(), "token with symbol already exists" );

    // add new currency in the stats table
    statstable.emplace( _self, [&]( auto& s ) {
    s.supply.symbol = maximum_supply.symbol;
    s.max_supply  = maximum_supply;
    s.issuer  = issuer;
    });
}

This action is used for creating a new currency by specifying the issuer account and the maximum supply for these tokens. Since maximum supply is of the type eosio::asset, it will handle storing symbol type for the new currency.

First we check if this action was indeed called by the contract account itself, using the require_auth(_self) statement. This function accepts the eoso::name object which refers to the account whose authority is to be checked. Then we check for invalid values for the inputs using eosio_assert(), which fails the transaction with the provided message if the given condition is violated. Finally if all the assertions were passed, we create new currency by persisting this information in the stats table. This is achieved using the emplace function of the multi-index tables. This function accepts two parameters, the first is the account that will pay for the RAM for storing this particular entry. Second parameter is a lambda expression, which is responsible for setting the values for the new row to be created. Thus after successful execution, we would have created a new token on the token contract.

Action: issue

void token::issue( name to, asset quantity, string memo )

{
    // check if symbol is valid and memo does not exceeds 256 bytes
    auto sym = quantity.symbol;
    eosio_assert( sym.is_valid(), "invalid symbol name" );
    eosio_assert( memo.size() <= 256, "memo has more than 256 bytes" );

    // check if token with this symbol name exists, and get iterator_
    stats statstable( _self, sym.code().raw() );
    auto existing = statstable.find( sym.code().raw() );
	eosio_assert( existing != statstable.end(), "token with symbol does not exist, create token before issue" );const  auto& st = *existing;

    // check for the authority of issuer and valid quantity_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");

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

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

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

This action is called by the currency issuer for issuing tokens to the desired accounts. Let us call the account getting the issued amount as ‘target account’.

Using eosio_assert(), we check for valid symbol types and quantities, and for authority of the issuer and if the specified token exists. For querying the multi-index table, we use the find method on the table instance. The find method returns the iterator that points towards the record if it was found, else it points to the end (sentinel) value of the table. If all assertions were passed, we need to update the balances of the target account, as well as increase the supply for our token type to reflect the increased circulation of the tokens. For updating the circulation, we increase the supply in the stats table for the target token and after that, we need to transfer

To do that, we need to call modify method on the stats table. This method accepts 3 parameters. First, it takes the iterator that points to the record we want to update (which we got during find operation). Second, it takes the RAM payer that will pay for the changes in the record. If the new changes might need extra RAM space, this account will pay for that. In our case, we have specified it as same_payer meaning the original payer account should be used (which is issuer in our case). Third argument is a lambda expression, that will make changes in the desired record.

Now we need to transfer tokens to target account. To do that, we initially add given amount of tokens in the issuer account and then call the transfer action from issuers account to the target account. This is done so that the tokens can be traced back to the issuer and do not seem to appear ‘magically’ in the target account. For that we make call to the private helper method add_balance(). This method adds the specified amount of asset to the given account and in our case, increases the balance for the issuer.

To transfer from the issuer to target account, we make call to the transfer action of the contract by issuing an inline action to the transfer method. In EOS smart contracts, we can initiate actions on our own accounts as well as the other accounts. To call the action from inside the contract, we use the macro SEND_INLINE_ACTION(). It takes 4 arguments; the account name of the contract, name of the method, associated permission and the data to be passed to the action. Here we send an inline action to the transfer action of the same contract. Thus tokens are finally issued to the target account.

Wrapping up the eosio.token contract**

We have covered the overall structure and most of the basic constructs that go into making smart contracts in general. This analysis will provide readers with enough prerequisite knowledge to analyse the remaining actions for the contract. The source file is given below for reference.

#include <eosio.token/eosio.token.hpp>

namespace eosio {

void token::create( 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.code().raw() );
    auto existing = statstable.find( sym.code().raw() );
    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( 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" );

    stats statstable( _self, sym.code().raw() );
	auto existing = statstable.find( sym.code().raw() );
    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, same_payer, [&]( auto& s ) {
    s.supply += quantity;
    });

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

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

void token::retire( 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" );

    stats statstable( _self, sym.code().raw() );auto existing = statstable.find( sym.code().raw() );
    eosio_assert( existing != statstable.end(), "token with symbol does not exist" );
    const  auto& st = *existing;

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

    eosio_assert( quantity.symbol == st.supply.symbol, "symbol precision mismatch" );

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

    sub_balance( st.issuer, quantity );
}

void token::transfer( name  from,
					  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.code();
    stats statstable( _self, sym.raw() );
    const  auto& st = statstable.get( sym.raw() );

    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" );

    auto payer = has_auth( to ) ? to : from;

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

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

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

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

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

void token::open( name owner, const symbol& symbol, name ram_payer )
{
    require_auth( ram_payer );
    auto sym_code_raw = symbol.code().raw();

    stats statstable( _self, sym_code_raw );
    const  auto& st = statstable.get( sym_code_raw, "symbol does not exist" );
    eosio_assert( st.supply.symbol == symbol, "symbol precision mismatch" );

    accounts acnts( _self, owner.value );
    auto it = acnts.find( sym_code_raw );
    if( it == acnts.end() ) {
	    acnts.emplace( ram_payer, [&]( auto& a ){
	    a.balance = asset{0, symbol};
	    });
    }
}

void token::close( name owner, const symbol& symbol )
{
    require_auth( owner );
    accounts acnts( _self, owner.value );
    auto it = acnts.find( symbol.code().raw() );
    eosio_assert( it != acnts.end(), "Balance row already deleted or never existed. Action won't have any effect." );
    eosio_assert( it->balance.amount == 0, "Cannot close because the balance is not zero." );
    acnts.erase( it );
}

} /// namespace eosio

EOSIO_DISPATCH( eosio::token, (create)(issue)(transfer)(open)(close)(retire) )

I hope that this article has been useful for the new EOS developers. Thanks for reading!


Stay tuned for the next part…

~~Part 1 — EEOS Smart Contract Development~~

~~Part 2 — EOS Smart Contracts Audit checklist to Keep In mind Before Development~~

~~Part 3- Understanding fundamental concepts for writing dApps on EOS.~~

~~Part 4- Analysing EOS standard token contract.~~

Part 5- Develop a basic crowd sale application with EOS .

Part 6- EOS for high performance dApps — Games on EOS!


Disclaimer: The views expressed by the author above do not necessarily represent the views of EOS GO. EOS GO is a community where EOS GO Blog being a platform for authors to express their diverse ideas and perspectives. To learn more about EOS GO and EOS, please join us on our social medias.

EOS GO Telegram - EOS News Channel - Twitter


EOS GO is funded by EOS ASIA and powered by YOU. Join the community and begin contributing to the movement by adding eos go to your name and joining the EOS GO telegram group.