{% note alert %}
This smart contract is presented here as an example, do not use it for production without prior verification.
{% endnote %}
If you don't have a local GlobalForce node installed, use the deployment tools at docs.globalforce.io
base.token is an advanced smart contract that implements a standard fungible token, similar to eosio.token, with additional features:
Account blacklisting: Prevent selected accounts from token operations.
Staking: Lock tokens on accounts for governance, DeFi, or utility.
Full ERC-20–like interface: Including issue, retire, transfer, open/close, and on-chain supply/balance lookups.
This contract is suitable for tokens requiring compliance, enhanced security, or staking utility directly in their core implementation.
Features Create, Issue, Retire, Transfer: Standard fungible token operations.
Blacklisting: Prevent specific accounts from receiving, sending, or otherwise interacting with tokens.
Staking: Lock tokens to prevent their transfer, for DeFi or other advanced tokenomics.
Open/Close Account Rows: RAM management for efficient DApps.
Multi-index Tables: All balances, supply, staked amounts, and blacklist are transparently stored on chain.
Strict Validations: Extensive checks for supply, balances, staked logic, and memo sizes.
create Create a new token (symbol/precision/max supply/issuer).
cleos push action base.token create '["issueracc", "1000000.0000 MYT"]' -p base.token
Issue (mint) tokens to the issuer account. Note: Only the issuer can issue tokens, and only to itself.
cleos push action base.token issue '["issueracc", "1000.0000 MYT", "Initial supply"]' -p issueracc
Burn (destroy) tokens from circulation. Only issuer may retire tokens.
cleos push action base.token retire '["100.0000 MYT", "burn"]' -p issueracc
Send tokens between accounts.
cleos push action base.token transfer '["alice", "bob", "25.0000 MYT", "For coffee"]' -p alice
Create a zero-balance row for an account (to pay RAM in advance, e.g., for DApp onboarding).
cleos push action base.token open '["bob", "4,MYT", "alice"]' -p alice
Remove a zero-balance row for an account (frees RAM).
cleos push action base.token close '["bob", "4,MYT"]' -p bob
Add or remove an account from blacklist. Only the contract account can perform this action.
cleos push action base.token blacklist '["baduser", true]' -p base.token
cleos push action base.token blacklist '["baduser", false]' -p base.token
Stake (lock) tokens on an account. Only the hardcoded admin ("testedemonft") may stake or unstake tokens.
cleos push action base.token stake '["alice", "100.0000 MYT"]' -p testedemonft
Unstake (release) previously staked tokens.
cleos push action base.token unstake '["alice", "50.0000 MYT"]' -p testedemonft
accounts (scope: user): Each account’s balance for each token symbol.
stat (scope: contract): Token statistics (supply, max supply, issuer) per symbol.
stake (scope: contract): Staked tokens per account.
blacklist (scope: contract): Blacklisted accounts (cannot transfer, receive, etc.).
Staked tokens are locked: The sum of staked amount and transfer amount may not exceed balance.
Blacklisted accounts: Any transfer, issue, open, or close attempt will fail if the account is blacklisted.
All arithmetic uses strict EOSIO asset checks.
Authorization enforced: Only the correct account can perform each action (e.g., issuer for issue/retire, contract for blacklist, account owner for transfer).
No over-issuance: Cannot issue more than max supply.
Blacklisted enforcement: Impossible to transfer to/from or open/close for blacklisted accounts.
Hardcoded admin for staking: Only testedemonft account can (un)stake (demo only; should be refactored for production).
get_supply(token_contract_account, symbol_code): Returns current on-chain token supply.
get_balance(token_contract_account, owner, symbol_code): Returns current on-chain token balance for an owner.
Deploy contract to an EOSIO account (e.g., base.token)
Create token with create
Issue initial supply with issue
Allow transfers by users
Stake tokens for governance/utility as needed
Blacklist accounts as a compliance or moderation tool
Limitations / To-Do Staking admin is hardcoded for demonstration; should be upgraded for governance/multisig.
No transfer memo validation (beyond 256 chars); for business use, more checks may be warranted.
Only basic staking (lock/unlock); could add rewards or other features as needed.
MIT or similar permissive open-source license.
Always audit contract for business or regulatory use.
For production staking, use governance or multi-signature accounts instead of a hardcoded admin.
DApps should use open to prepay RAM for new users.
Frontends should monitor blacklist for compliance.
#include <eosio/eosio.hpp>
#include <eosio/asset.hpp>
#include <eosio/transaction.hpp>
#include <eosio/system.hpp>
#include <eosio/crypto.hpp>
#include <eosio/action.hpp>
#include <eosio/print.hpp>
using namespace eosio;
using namespace std;
using std::string;
using std::vector;
#include "basetoken.hpp"
namespace eosio {
/**
* @brief Action: Create a new token, defining its issuer and maximum supply.
* Can only be called by contract itself.
*/
void basetoken::create( const name& issuer, const asset& maximum_supply )
{
require_auth( get_self() ); // Only contract can create tokens
auto sym = maximum_supply.symbol;
check( sym.is_valid(), "Invalid symbol name" );
check( maximum_supply.is_valid(), "Invalid supply");
check( maximum_supply.amount > 0, "Max-Supply must be more then 0");
stats statstable( get_self(), sym.code().raw() );
auto existing = statstable.find( sym.code().raw() );
check( existing == statstable.end(), "This token already exist" );
// Create the stat row for this symbol
statstable.emplace( get_self(), [&]( auto& s ) {
s.supply.symbol = maximum_supply.symbol;
s.max_supply = maximum_supply;
s.issuer = issuer;
});
}
/**
* @brief Action: Issue (mint) tokens, increasing total supply.
* Only issuer can issue, and only to their own account.
*/
void basetoken::issue( const name& to, const asset& quantity, const string& memo )
{
getblacklist( to ); // Prevent blacklisted issuance
auto sym = quantity.symbol;
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(), "This token dont exist." );
const auto& st = *existing;
check( to == st.issuer, "Token can only be issued TO issuer account" );
require_recipient( to );
require_auth( st.issuer );
check( quantity.is_valid(), "Invalid quantity" );
check( quantity.amount > 0, "Amount should be more then 0" );
check( quantity.symbol == st.supply.symbol, "Symbol precision mismatch" );
check( quantity.amount <= st.max_supply.amount - st.supply.amount, "Quantity exceeds available supply");
// Increase supply and add to issuer's balance
statstable.modify( st, same_payer, [&]( auto& s ) {
s.supply += quantity;
});
add_balance( st.issuer, quantity, st.issuer );
}
/**
* @brief Action: Retire (burn) tokens, reducing total supply.
* Only issuer can retire.
*/
void basetoken::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" );
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, "Amount should be more then 0" );
check( quantity.symbol == st.supply.symbol, "Symbol precision mismatch" );
statstable.modify( st, same_payer, [&]( auto& s ) {
s.supply -= quantity;
});
sub_balance( st.issuer, quantity );
}
/**
* @brief Action: Transfer tokens between accounts.
* Enforces blacklist, staked amounts, and minimum checks.
*/
void basetoken::transfer( name from, name to, asset quantity, string memo )
{
check( from != to, "Cannot transfer to self" );
require_auth( from );
getblacklist( from );
getblacklist( to );
check( is_account( to ), "TO ["+to.to_string()+"] account does not exist");
auto sym = quantity.symbol.code();
stats statstable( get_self(), sym.raw() );
const auto& st = statstable.get( sym.raw() );
require_recipient( from );
require_recipient( to );
check_quantity( 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;
get_staked_balance( from, quantity ); // Ensure enough non-staked tokens
sub_balance( from, quantity );
add_balance( to, quantity, payer );
}
/**
* @brief Subtracts tokens from account balance.
*/
void basetoken::sub_balance( const name owner, const asset value ) {
accounts from_acnts( get_self(), owner.value );
const auto& from = from_acnts.find( value.symbol.code().raw() );
if( from == from_acnts.end() ) {
check( false, "FROM ["+owner.to_string()+"] dont have ["+value.symbol.code().to_string()+"] tokens" );
}else{
check( from->balance.amount >= value.amount, "Overdraw balance on token ["+value.symbol.code().to_string()+"] on ["+owner.to_string()+"]" );
from_acnts.modify( from, owner, [&]( auto& a ) {
a.balance -= value;
});
}
}
/**
* @brief Adds tokens to account balance, creating row if needed.
*/
void basetoken::add_balance( const name owner, const asset value, const name ram_payer )
{
accounts to_acnts( get_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;
});
}
}
/**
* @brief Checks if enough non-staked tokens are available before transfer.
*/
void basetoken::get_staked_balance( const name from, asset quantity ){
accounts from_acnts( get_self(), from.value );
auto balance = from_acnts.find( quantity.symbol.code().raw() );
if( balance != from_acnts.end() ){
stakeds staked( get_self(), get_self().value );
auto stake = staked.find( from.value );
if( stake != staked.end() ) {
if( balance->balance.amount < ( quantity.amount + stake->staked.amount ) ){
check( false, "FROM ["+from.to_string()+"] account have staked amount ["+std::to_string( stake->staked.amount )+"]. Balance is less than possible transfer." );
}
}
}
}
/**
* @brief Action: Open a token balance row for an account and symbol.
*/
void basetoken::open( const name& owner, const symbol& symbol, const name& ram_payer )
{
require_auth( ram_payer );
getblacklist( owner );
getblacklist( ram_payer );
check( is_account( owner ), "owner ["+owner.to_string()+"] account does not exist" );
auto sym_code_raw = symbol.code().raw();
stats statstable( get_self(), sym_code_raw );
const auto& st = statstable.get( sym_code_raw, "Symbol does not exist" );
check( st.supply.symbol == symbol, "Symbol precision mismatch" );
accounts acnts( get_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};
});
}
}
/**
* @brief Action: Close a token balance row for an account (balance must be zero).
*/
void basetoken::close( const name& owner, const symbol& symbol )
{
require_auth( owner );
getblacklist( owner );
accounts acnts( get_self(), owner.value );
auto it = acnts.find( symbol.code().raw() );
check( it != acnts.end(), "Balance row already deleted or never existed. Action won't have any effect." );
check( it->balance.amount == 0, "Cannot close because the balance is not zero." );
acnts.erase( it );
}
/**
* @brief Action: Stake tokens (only callable by privileged account for demo).
* Staked tokens are locked and cannot be transferred.
*/
void basetoken::stake( const name from, const asset quantity )
{
require_auth( "testedemonft"_n ); // Hardcoded admin for demonstration
check_quantity( quantity );
accounts from_acnts( get_self(), from.value );
auto balance = from_acnts.find( quantity.symbol.code().raw() );
if( balance != from_acnts.end() ){
if( balance->balance.amount < quantity.amount ){
check( false, "FROM ["+from.to_string()+"] balance ["+std::to_string( balance->balance.amount )+"] is less than want stake ["+std::to_string( quantity.amount )+"]" );
}
}else{
check( false, "FROM ["+from.to_string()+"] account have balance" );
}
stakeds staked( get_self(), get_self().value );
auto to = staked.find( from.value );
if( to == staked.end() ) {
staked.emplace( get_self(), [&]( auto& t ){
t.account = from;
t.staked = quantity;
});
} else {
staked.modify( to, get_self(), [&]( auto& t ) {
t.staked += quantity;
});
}
}
/**
* @brief Action: Unstake tokens, releasing them for transfer.
*/
void basetoken::unstake( const name from, const asset quantity )
{
require_auth( "testedemonft"_n ); // Hardcoded admin for demonstration
check_quantity( quantity );
stakeds staked( get_self(), get_self().value );
auto stake = staked.find( from.value );
if( stake == staked.end() ){
check( false, "FROM ["+from.to_string()+"] account does have stake" );
}else{
if( stake->staked.amount == quantity.amount ){
staked.erase( stake );
}else if( stake->staked.amount < quantity.amount ){
check( false, "FROM ["+from.to_string()+"] account have less staked amount ["+std::to_string( stake->staked.amount )+"] than want unstake ["+std::to_string( quantity.amount )+"]" );
}else{
staked.modify( stake, get_self(), [&]( auto& a ) {
a.staked -= quantity;
});
}
}
}
/**
* @brief Internal utility: Check if an account is blacklisted and abort if true.
*/
void basetoken::getblacklist( name account ){
db_blacklist blacklist(get_self(), get_self().value);
auto black = blacklist.find( account.value );
if(black != blacklist.end()){
check(false, "Account is on BLACKLIST");
}
}
/**
* @brief Action: Add or remove an account from the blacklist (contract-only).
* Notifies the account.
*/
void basetoken::blacklist( name account, bool a ){
require_auth(get_self());
require_recipient( account );
if( account == get_self()){
check(false, "SELF not should be added");
}
db_blacklist blacklist(get_self(), get_self().value);
auto black = blacklist.find( account.value );
if(black == blacklist.end()){
if(!a){ check(false, "Account dont exist"); }
blacklist.emplace(get_self(), [&](auto& t){
t.account = account;
});
}else{
if(a){
check(false, "Account already exist");
}else{
blacklist.erase(black);
}
}
}
/**
* @brief Internal utility: Checks validity of a quantity (positive, valid, symbol valid).
*/
void basetoken::check_quantity( asset quantity ){
auto sym = quantity.symbol;
check( quantity.is_valid(), "Invalid quantity" );
check( sym.is_valid(), "Invalid symbol name" );
check( quantity.amount > 0, "Quantity must be positive");
}
} // namespace eosio
#pragma once
// ---- Standard EOSIO headers for smart contract development ----
#include <eosio/eosio.hpp> // EOSIO contract, macro, and ABI definitions
#include <eosio/print.hpp> // For debugging (not required in production)
#include <eosio/asset.hpp> // Asset and symbol types
#include <eosio/transaction.hpp> // Transaction utilities (not heavily used here)
#include <eosio/action.hpp> // Action construction utilities
using namespace eosio;
using namespace std;
using std::string;
using std::vector;
/**
* @namespace eosio
* EOSIO C++ API namespace. All contract code is under this.
*/
namespace eosio {
/**
* @class basetoken
* @brief A basic EOSIO token contract, with support for blacklisting, staking, and standard actions.
*
* Features:
* - ERC-20 like interface (create, issue, retire, transfer, open, close)
* - Blacklist: prevent listed accounts from actions
* - Staking: lock tokens on accounts for DeFi/gov use-cases
*/
class [[eosio::contract("base.token")]] basetoken : public contract {
public:
using contract::contract;
// --- Standard Token Actions (all exposed as public EOSIO actions) ---
/**
* @brief Create a new token with the specified maximum supply and issuer.
* Only contract itself can call.
* @param issuer Who will control issuance of tokens
* @param maximum_supply The maximum allowable supply (asset, includes symbol and precision)
*/
[[eosio::action]]
void create( const name& issuer, const asset& maximum_supply);
/**
* @brief Issue new tokens to the issuer's account. Only issuer may call.
* @param to Account to receive issued tokens (must be issuer!)
* @param quantity Amount of tokens to issue
* @param memo Arbitrary string memo (max 256 bytes)
*/
[[eosio::action]]
void issue( const name& to, const asset& quantity, const string& memo );
/**
* @brief Retire tokens, removing them from circulation. Only issuer may call.
* @param quantity Amount of tokens to burn/retire
* @param memo Arbitrary memo
*/
[[eosio::action]]
void retire( const asset& quantity, const string& memo );
/**
* @brief Standard token transfer between accounts.
* @param from Sender (must authorize)
* @param to Recipient
* @param quantity Amount to transfer
* @param memo Arbitrary memo (max 256 bytes)
*/
[[eosio::action]]
void transfer( const name from, const name to, const asset quantity, const string memo );
/**
* @brief Open a balance row for a given symbol on behalf of owner (required for RAM payer logic).
* @param owner Who will own the new token balance row
* @param symbol The token symbol
* @param ram_payer Who pays for RAM usage
*/
[[eosio::action]]
void open( const name& owner, const symbol& symbol, const name& ram_payer );
/**
* @brief Close a balance row for a given symbol if balance is zero.
* @param owner Account who owns the row (must authorize)
* @param symbol Token symbol
*/
[[eosio::action]]
void close( const name& owner, const symbol& symbol );
/**
* @brief Add or remove an account from the blacklist.
* @param account Account to add/remove
* @param a true = add to blacklist, false = remove from blacklist
*/
[[eosio::action]]
void blacklist( name account, bool a );
/**
* @brief Stake tokens, locking them for advanced use-cases.
* @param from Account to stake from (must own tokens)
* @param quantity Amount to stake
*/
[[eosio::action]]
void stake( const name from, const asset quantity );
/**
* @brief Unstake previously staked tokens.
* @param from Account to unstake from (must own stake)
* @param quantity Amount to unstake
*/
[[eosio::action]]
void unstake( const name from, const asset quantity );
// --- Static Utility Methods for DApps and Scripts ---
/**
* @brief Get the current supply for a symbol from on-chain stats table.
*/
static asset get_supply( const name& token_contract_account, const symbol_code& sym_code )
{
stats statstable( token_contract_account, sym_code.raw() );
const auto& st = statstable.get( sym_code.raw() );
return st.supply;
}
/**
* @brief Get the balance of a specific owner for a symbol from the on-chain table.
*/
static asset get_balance( const name& token_contract_account, const name& owner, const symbol_code& sym_code )
{
accounts accountstable( token_contract_account, owner.value );
const auto& ac = accountstable.get( sym_code.raw() );
return ac.balance;
}
// --- EOSIO ABI Action Wrappers ---
using create_action = eosio::action_wrapper<"create"_n, &basetoken::create>;
using issue_action = eosio::action_wrapper<"issue"_n, &basetoken::issue>;
using retire_action = eosio::action_wrapper<"retire"_n, &basetoken::retire>;
using transfer_action = eosio::action_wrapper<"transfer"_n, &basetoken::transfer>;
using open_action = eosio::action_wrapper<"open"_n, &basetoken::open>;
using close_action = eosio::action_wrapper<"close"_n, &basetoken::close>;
using blacklist_action = eosio::action_wrapper<"blacklist"_n, &basetoken::blacklist>;
using stake_action = eosio::action_wrapper<"stake"_n, &basetoken::stake>;
using unstake_action = eosio::action_wrapper<"unstake"_n, &basetoken::unstake>;
private:
// --- Internal contract utility functions (not exposed as actions) ---
void getblacklist( name account ); // Checks if account is blacklisted; aborts if true
void sub_balance( const name owner, const asset value ); // Subtracts value from owner's balance
void add_balance( const name owner, const asset value, const name ram_payer ); // Adds value to owner's balance
void get_staked_balance( const name from, asset quantity ); // Checks if sufficient non-staked tokens available
void check_quantity( asset quantity ); // Checks quantity is valid and positive
// --- On-chain Multi-Index Table Definitions ---
/**
* @struct account
* Per-account, per-symbol token balance table (standard EOSIO token pattern)
*/
struct [[eosio::table]] account {
asset balance;
uint64_t primary_key()const { return balance.symbol.code().raw(); }
};
/**
* @struct currency_stats
* Singleton stats table for each token symbol
*/
struct [[eosio::table]] currency_stats {
asset supply;
asset max_supply;
name issuer;
uint64_t primary_key()const { return supply.symbol.code().raw(); }
};
/**
* @struct currency_stake
* Per-account staking info (amount staked)
*/
struct [[eosio::table]] currency_stake {
name account;
asset staked;
uint64_t primary_key()const { return account.value; }
};
/**
* @struct blacklists
* Stores all blacklisted accounts (preventing them from certain actions)
*/
struct [[eosio::table]] blacklists {
name account;
uint64_t primary_key() const { return account.value; }
};
// Multi-index types
typedef eosio::multi_index< "accounts"_n, account > accounts;
typedef eosio::multi_index< "stat"_n, currency_stats > stats;
typedef eosio::multi_index< "stake"_n, currency_stake > stakeds;
typedef eosio::multi_index< "blacklist"_n, blacklists > db_blacklist;
};
} // namespace eosio
{% note alert %} This smart contract is presented here as an example, do not use it for production without prior verification. {% endnote %} If you don't have a