{% 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
Download stablecoin.hpp
Download stablecoin.cpp
stablecoin is a token contract for GlobalForce/EOSIO/Antelope that provides standard operations (create, issue, transfer, burn), as well as mechanisms for contract pause (pause/unpause) and account blacklist (blacklist/unblacklist). The contract stores balances in accounts, issue metadata in stat, pause status in pausetable, and blocked accounts in blacklists. stablecoin
Create a token with a given symbol, precision, and maximum emission.
Issue (mint) of tokens by the issuer; credit to the issuer's balance with an optional internal transfer to a third party.
Transfers between accounts with checks (accounts exist, are not blocked, the contract is not paused, correct amounts/symbols).
Burn by the issuer with a decrease in supply and max_supply.
Pause/unpause the contract. In pause mode, user transfers should be prohibited.
Blacklist: add/remove accounts to block any operations with the token.
get_supply / get_balance utilities for reading the current supply and balances.
Stores the balance of each account by token symbol.
balance (asset) — token balance; primary key — symbol.code().raw().
stat (currency_stats)
Token metadata by symbol:
List of blacklisted accounts:
account (name) — blacklisted account (primary key: account.value).
Pause status:
Conditions:
Only the contract account can call create.
maximum_supply > 0, valid symbol/asset.
Symbol is not registered in stat yet.
cleos push action stablecoin create '["issueracct","1000000.0000 STC"]' -p stablecoin@active
issue(to, quantity, memo)
Issues (mint) the specified number of tokens.
Logic:
Only issuer from stat can issue.
quantity > 0; symbol must match symbol from stat.
Increases supply; in this code, when supply > max_supply overflows, it raises max_supply to the current supply (this behavior can be disabled if "limit expansion" is unacceptable).
Adds tokens to the issuer's balance; if to != issuer, it performs an internal transfer transfer(issuer → to).
cleos push action stablecoin issue '["issueracct","50000.0000 STC","initial"]' -p issueracct@active
transfer(from, to, quantity, memo)
Transfer tokens between accounts.
Checks:
Cannot transfer to yourself; from authorizes the transaction.
Account to must exist.
Both accounts must not be blacklisted.
Transfers must be blocked in pause mode (see the Pause section below).
quantity is valid, positive, and the symbol matches stat.
Memo ≤ 256 bytes.
Writes off from, credits to (RAM pays to if it authorizes; otherwise, from).
Example:
cleos push action stablecoin transfer '["alice","bob","12.3456 STC","payment"]' -p alice@active
burn(quantity, memo)
Burns tokens, reducing supply and max_supply; writes off the issuer's balance.
Conditions:
Only issuer can call;
quantity > 0; does not exceed current supply.
Example:
cleos push action stablecoin burn '["100.0000 STC","reduce supply"]' -p issueracct@active
pause() and unpause()
pause() — enables pause mode: adds/updates an entry in pausetable (id=1, paused=true).
unpause() — removes pause: clears pausetable.
Can only be called by contract account.
Examples:
cleos push action stablecoin pause '[]' -p stablecoin@active
cleos push action stablecoin unpause '[]' -p stablecoin@active
[!NOTE] ⚠️ Important about the pause check: the code implements the helper function is_paused() and a check in transfer. The semantics should be "if the contract is paused - prohibit the transfer". Make sure in the review that the condition in transfer interprets is_paused() correctly to block transfers when paused.
blacklist(account, memo) and unblacklist(account)
blacklist - adds an account to the blacklist; contract only; memo ≤ 256 bytes; re-addition is prohibited.
unblacklist - removes an account from the list; contract only; requires that the record exists.
Accounts in the list cannot send/receive tokens.
Examples:
cleos push action stablecoin blacklist '["badguy","fraud investigation"]' -p stablecoin@active
cleos push action stablecoin unblacklist '["badguy"]' -p stablecoin@active
Internal functions (balances)
sub_balance(owner, value) — safely decreases the balance; if zero, deletes the line in accounts.
add_balance(owner, value, ram_payer) — creates/replenishes the balance; RAM pays ram_payer.
get_supply(token_contract, sym_code) — reads the current offer from stat.
get_balance(token_contract, owner, sym_code) — reads the owner balance from accounts.
Build a contract with EOSIO.CDT:
eosio-cpp -abigen -o stablecoin.wasm stablecoin.cpp
Upload the code to the contract account:
cleos set contract stablecoin /path/to/build -p stablecoin@active
Create a token create, then issue an issue.
Project start: create → issue to issuer → transfer to users.
Emergency stop of transfers: pause → investigation/fixes → unpause.
Compliance: if abuse is detected — blacklist the account; after risks are removed — unblacklist.
Submission reduction: burn at issuer.
Actions create, pause/unpause, blacklist/unblacklist should be called only by the contract account; issue/burn — only by the issuer.
Test the pause logic in transfer on testnet to ensure correct blocking of transfers when paused=true.
Monitor RAM costs during mass transfers (creating new lines in accounts).
Current issue behavior can automatically raise max_supply if supply exceeds the limit. If a strict limit is needed, remove this block.
Add events/notifications (inline logging) for auditing.
Consider roles/multisig for admin actions.
MIT or similarly permissive license.
#pragma once
#include <eosiolib/asset.hpp>
#include <eosiolib/eosio.hpp>
#include <string>
using namespace eosio;
using std::string;
/**
* The stablecoin contract is an implementation of its own token with standard capabilities,
* as well as support for a blacklist of accounts and contract pause.
*/
CONTRACT stablecoin : public contract {
public:
using contract::contract;
/**
* Token creation.
* issuer — issuer (token owner, who has the right to issue).
* maximum_supply — maximum token issue volume.
*/
ACTION create( name issuer, asset maximum_supply );
/**
* Token emission (mint).
* to — recipient account.
* quantity — quantity of tokens.
* memo — arbitrary comment.
*/
ACTION issue( name to, asset quantity, string memo );
/**
* Transfer tokens between accounts.
* from — sender.
* to — recipient.
* quantity — quantity.
* memo — arbitrary comment.
*/
ACTION transfer( name from, name to, asset quantity, string memo );
/**
* Token burning by the issuer.
* quantity — the number of tokens to be destroyed.
* memo — an arbitrary comment.
*/
ACTION burn( asset quantity, string memo );
/**
* Pause the contract.
* All transfers are blocked (except the pause/unpause method itself).
* Only the contract account can call.
*/
ACTION pause();
/**
* Unpause contract (allow transfers).
* Only contract account can call.
*/
ACTION unpause();
/**
* Adding an account to the blacklist.
* An account in the blacklist cannot send/receive tokens.
* memo — the reason for blocking.
*/
ACTION blacklist( name account, string memo );
/**
* Removing an account from the blacklist.
*/
ACTION unblacklist( name account );
/**
* Get the current supply (emission) of the token.
*/
static asset get_supply( name token_contract_account, symbol_code sym ) {
stats statstable( token_contract_account, sym.raw() );
const auto& st = statstable.get( sym.raw() );
return st.supply;
}
/**
* Get the balance of a specific account using the token symbol code.
*/
static asset get_balance( name token_contract_account, name owner, symbol_code sym ) {
accounts accountstable( token_contract_account, owner.value );
const auto& ac = accountstable.get( sym.raw() );
return ac.balance;
}
private:
/**
* Account balance table.
* Each account and token has a balance stored.
*/
TABLE account {
asset balance; // Balance in this token
uint64_t primary_key()const { return balance.symbol.code().raw(); }
};
/**
* Table with information about each token (emission statistics).
* Includes supply, max_supply, issuer.
*/
TABLE currency_stats {
asset supply; // Current offer
asset max_supply; // Maximum permitted emission
name issuer; // Token issuer
uint64_t primary_key()const { return supply.symbol.code().raw(); }
};
/**
* Account blacklist table.
* Accounts from here cannot perform operations with the token.
*/
TABLE blacklist_table {
name account; // Blocked account
auto primary_key() const { return account.value; }
};
/**
* Table for storing the contract pause status.
*/
TABLE pause_table {
uint64_t id; // Always 1, single line
bool paused; // True if the contract is paused
auto primary_key() const { return id; }
};
// Definitions of multi_index tables for access within a contract
typedef eosio::multi_index< "accounts"_n, account > accounts;
typedef eosio::multi_index< "stat"_n, currency_stats > stats;
typedef eosio::multi_index< "blacklists"_n, blacklist_table > blacklists;
typedef eosio::multi_index< "pausetable"_n, pause_table > pausetable;
/**
* Internal method: decrease account balance (called on transfers/burning).
*/
void sub_balance( name owner, asset value );
/**
* Internal method: increase account balance (called during transfers/issues).
*/
void add_balance( name owner, asset value, name ram_payer );
/**
* Internal method: Check if the contract is in paused state.
*/
bool is_paused();
};
#include "stablecoin.hpp"
/**
* Action: create a new token.
* - Only the contract itself can create a token.
* - Checks that the parameters are correct and that there is no token with that symbol.
*/
ACTION stablecoin::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; // начальная эмиссия = 0
s.max_supply = maximum_supply;
s.issuer = issuer;
});
}
/**
* Action: issue new tokens.
* - Can only be called by the token issuer.
* - The issued amount is added to the issuer's balance and increases the total supply.
* - Only a positive number of tokens can be issued.
* - If the specified recipient is not the issuer, an internal transfer is immediately called.
*/
ACTION stablecoin::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" );
// We increase supply, as well as max_supply, if supply suddenly goes beyond max_supply (it can be removed if there is no need to "expand" the limit)
statstable.modify( st, same_payer, [&]( auto& s ) {
s.supply += quantity;
if ( s.supply > s.max_supply ) {
s.max_supply = s.supply;
}
});
add_balance( st.issuer, quantity, st.issuer );
if( to != st.issuer ) {
// If the recipient is not the issuer, we transfer tokens to him (from the issuer)
SEND_INLINE_ACTION( *this, transfer, {st.issuer, "active"_n}, {st.issuer, to, quantity, memo} );
}
}
/**
* Action: transfer tokens between accounts.
* - Rejects if the contract is paused or if the sender/recipient is blacklisted.
* - Disables transfers to yourself.
* - Checks for the presence of the recipient account.
*/
ACTION stablecoin::transfer( name from, name to, asset quantity, string memo ) {
eosio_assert( is_paused(), "contract is paused." );
blacklists blacklistt(_self, _self.value);
auto fromexisting = blacklistt.find( from.value );
eosio_assert( fromexisting == blacklistt.end(), "account blacklisted(from)" );
auto toexisting = blacklistt.find( to.value );
eosio_assert( toexisting == blacklistt.end(), "account blacklisted(to)" );
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 );
}
/**
* Action: Token burning.
* - Only the issuer can burn tokens.
* - Decreases the total supply and max_supply.
* - Writes off tokens from the issuer's balance.
*/
ACTION stablecoin::burn(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.code();
stats statstable( _self, sym_name.raw() );
auto existing = statstable.find( sym_name.raw() );
eosio_assert( existing != statstable.end(), "token with symbol does not exist, create token before burn" );
const auto& st = *existing;
require_auth( st.issuer );
eosio_assert( quantity.is_valid(), "invalid quantity" );
eosio_assert( quantity.amount > 0, "must burn positive or zero quantity" );
eosio_assert( quantity.symbol == st.supply.symbol, "symbol precision mismatch" );
eosio_assert( quantity.amount <= st.supply.amount, "quantity exceeds available supply");
statstable.modify( st, same_payer, [&]( auto& s ) {
s.supply -= quantity;
s.max_supply -= quantity;
});
sub_balance( st.issuer, quantity );
}
/**
* Action: Pause the contract.
* - Only allowed by the contract account.
* - Adds/modifies an entry in the pausetable with paused = true.
*/
ACTION stablecoin::pause() {
require_auth( _self );
pausetable pauset(_self, _self.value);
auto itr = pauset.find(1);
if (itr != pauset.end()) {
pauset.modify(itr, _self, [&](auto& p) {
p.paused = true;
});
} else {
pauset.emplace(_self, [&](auto& p) {
p.id = 1;
p.paused = true;
});
}
}
/**
* Action: Unpause contract (allow transfers).
* - Only allowed for contract account.
* - Clears pause table.
*/
ACTION stablecoin::unpause() {
require_auth( _self );
pausetable pauset(_self, _self.value);
while (pauset.begin() != pauset.end()) {
auto itr = pauset.end();
itr--;
pauset.erase(itr);
pausetable pauset(_self, _self.value);
}
}
/**
* Action: add account to blacklist.
* - Allowed only for contract account.
* - Prohibits specified account from any operations with token.
*/
ACTION stablecoin::blacklist( name account, string memo ) {
require_auth( _self );
eosio_assert( memo.size() <= 256, "memo has more than 256 bytes" );
blacklists blacklistt(_self, _self.value);
auto existing = blacklistt.find( account.value );
eosio_assert( existing == blacklistt.end(), "blacklist account already exists" );
blacklistt.emplace( _self, [&]( auto& b ) {
b.account = account;
});
}
/**
* Action: remove account from blacklist.
* - Allowed only for contract account.
*/
ACTION stablecoin::unblacklist( name account) {
require_auth( _self );
blacklists blacklistt(_self, _self.value);
auto existing = blacklistt.find( account.value );
eosio_assert( existing != blacklistt.end(), "blacklist account not exists" );
blacklistt.erase(existing);
}
/**
* Internal method: decrease the owner account balance by value.
* If the balance becomes zero, the record is deleted.
*/
void stablecoin::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" );
if( from.balance.amount == value.amount ) {
from_acnts.erase( from );
} else {
from_acnts.modify( from, owner, [&]( auto& a ) {
a.balance -= value;
});
}
}
/**
* Internal method: increase owner balance by value.
* If there was no account, a new record is created, otherwise the amount is increased.
* ram_payer — who pays for the memory for the new record.
*/
void stablecoin::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;
});
}
}
/**
* Internal method: increase owner balance by value.
* If there was no account, a new record is created, otherwise the amount is increased.
* ram_payer — who pays for the memory for the new record.
*/
bool stablecoin::is_paused() {
pausetable pauset(_self, _self.value);
bool existing = ( pauset.find( 1 ) == pauset.end() );
return existing;
}
/**
* EOSIO_DISPATCH macro - registers all contract actions for external calling.
*/
EOSIO_DISPATCH( stablecoin, (create)(issue)(transfer)(burn)(pause)(unpause)(blacklist)(unblacklist) )
{% 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