Exchanger Contract

Warning

This contract is for educational purposes only.

In this tutorial we will create a p2p exchanger contract with a virtual wallet, you will learn how to include other contracts and use the external actions.
We will designate 2 entities in our contract, the first entity is the creator of new offers for the buy/sell, second is executor of open offers for the buy/sell.
Unprivileged accounts are not allowed to use tokens of other accounts within the actions, so we need to create a virtual wallet on our contract, with actions for depositing funds and withdrawing.

Let’s Define Contract Actions and Tables:

Exchanger contract actions:

-deposit increase the balance of a virtual contract wallet
-withdraw withdraw funds from the virtual wallet of the contract to the account
-createoffer create an offer to buy or sell tokens
-canceloffer cancellation of an already created offer to buy or sell
-executeoffer execute an already created offer

Exchanger contract tables:

-accounts balances of accounts on the virtual wallets on the contract
-offers all open offers to buy or sell

Create a skeleton of a new project

Note

If you are not familiar with the structure of a smart contract, you can get acquainted here.

Let’s create a contract skeleton:

$ eosio-init --path=. --project=exchanger

The structure of our project will look as follows:

├── build
├── CMakeLists.txt
├── include
│   └── exchanger.hpp
├── README.txt
├── ricardian
│   └── exchanger.contracts.md
└── src
    ├── CMakeLists.txt
    └── exchanger.cpp

Before the next step, delete the code generated by eosio-init.

Create a C++ class

Firstly, create a standard C++ class called exchanger that extends eosio::contract and exchanger_contract namespace. Using this declaration will allow us to write more concise code.

include/exchanger.hpp:

#include <eosio/eosio.hpp>

namespace exchanger_contract {

    using namespace eosio;
    using namespace std;

    class [[eosio::contract("exchanger")]] exchanger : public eosio::contract {
      public:
      private:
    }
}

Now, let’s denote the actions that we have already defined above.

createoffer

It must accept the following arguments:

-account name of the account that creates the offer
-offer_type type of offer being created (buy or sell)
-quantity quantity of tokens to buy or sell
-price buy or sell price of one token
void createoffer(const name& account, const name& offer_type, const asset& quantity, const double& price);

canceloffer

It must accept the following arguments:

-account account name that cancels an already created offer
-offer_type type of offer being canceled (buy or sell)
void canceloffer(const name& account, const name& offer_type);

executeoffer

It must accept the following arguments:

-account name of the account that executes the offer
-offer_type type of offer being executed (buy or sell)
-seller name of the account that submitted the buy or sell offer
-quantity quantity of tokens to buy or sell
void executeoffer(const name& account, const name& offer_type, const name& seller, const asset& quantity);

deposit

Deposit function will be triggered by the rem.token by using on_notify attribute. To accept a transfer from the account we need to have a deposit action.

Note

The on_notify attribute is one of the EOSIO.CDT attributes that annotates a smart contract action.
Annotating an action with an on_notify attribute ensures any incoming notification is forwarded to the annotated action if and only if the notification is dispatched from a specified contract and from a specified action.
In this case, the on_notify attribute ensures the incoming notification is forwarded to the deposit action only if the notification comes from the rem.token contract and is from the rem.token's transfer action.

Refer to EOSIO’s on_notify attribute.

It must accept the following arguments:

-account replenishing account
-contract exchanger contract name
-quantity quantity of tokens to be replenished
-memo transfer action memo
void deposit(const name& account, const name& exchanger_contract, const asset& quantity, const string& memo);

withdraw

It must accept the following arguments:

-account name of the account that withdraws funds from the virtual wallet of the contract
-quantity quantity of tokens to be withdrawn
void withdraw(const name& account, const asset& quantity);

Let’s add these actions to our class:

#include <eosio/eosio.hpp>

namespace exchanger_contract {

    using namespace eosio;

    class [[eosio::contract("exchanger")]] exchanger : public eosio::contract {
      public:
        using contract::contract;

        [[eosio::action]]
        void createoffer(const name& account, const name& offer_type, const asset& quantity, const double& price);

        [[eosio::action]]
        void canceloffer(const name& account, const name& offer_type);

        [[eosio::action]]
        void executeoffer(const name& account, const name& offer_type, const name& seller, const asset& quantity);

        [[eosio::action]]
        void withdraw(const name& account, const asset& quantity);

        [[eosio::on_notify("rem.token::transfer")]]
        void deposit(const name& account, const name& exchanger_contract, const asset& quantity, const string& memo);
      private:
    }
}

Now, let’s denote the tables that we have already defined above:

account table

-balance account balance
-is_locked is the balance locked
struct [[eosio::table]] account {
   asset    balance;
   bool     is_locked;

   uint64_t primary_key()const { return balance.symbol.code().raw(); }
};
In this multiple index table declaration, a new type called asset is used. An asset is a type designed to represent a digital token asset. See more details in the asset reference documentation.
To use type asset add include:
#include <eosio/asset.hpp>

offers table

-account name of the account that creates the offer
-quantity quantity of tokens to buy or sell
-price buy or sale price of one token
  • offer_timestamp - offer creation time, optional field for table
struct [[eosio::table]] offers {
      name              account;
      asset             quantity;
      double            price;
      block_timestamp   offer_timestamp;

      uint64_t primary_key() const { return account.value; }
      uint64_t by_amount() const { return quantity.amount; }
      double by_price() const { return price; }
   };

Tip

For improved compilation time, you can add to table explicit serialization macro EOSLIB_SERIALIZE.
More information about EOSLIB_SERIALIZE.

Let’s define indexes for our tables:

typedef eosio::multi_index< "accounts"_n, account > accounts;
typedef multi_index<"offers"_n, offers,
   indexed_by<"byamount"_n, const_mem_fun < offers, uint64_t, &offers::by_amount>>,
   indexed_by<"byprice"_n, const_mem_fun < offers, double, &offers::by_price>>
   > offers_idx;

Note

You can read more about the syntax for defining indexes and tables on EOSIO:

Let’s define enumeration for offer type, for convenience.
As types of offers, we will use the eosio type name and values buy and sell. Type name can be converted to uint64_t, let’s use it in our enumeration:
enum class offer_type : uint64_t { buy = "buy"_n.value, sell = "sell"_n.value };

Now, we define the symbol that will be accepted by the token purchase fee.

For example, the symbol REM is the main symbol for the exchange, then the symbol SYS will be paid for the purchase of the symbol REM.

static constexpr symbol sys_symbol{"SYS", 4};
In order to change the virtual balance of wallets, let’s define methods: add_balance and sub_balance.
Also, let’s define a method lock_wallet.

We need a lock_wallet method for securing purposes. It is necessary to prohibit the user from spending tokens that are used by an open offer

Let’s add these three methods:

void add_balance(const name& account, const asset& value);
void sub_balance(const name& account, const asset& value);
void lock_wallet(const bool& lock_wallet, const name& account, const asset& value);

lock_wallet can both lock and unlock a wallet, if variable lock_wallet=true then it will lock wallet, if lock_wallet=false it will unlock wallet.

Now the file exchanger.hpp will look like this:

#pragma once

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

namespace exchanger_contract {

   using namespace eosio;
   using namespace std;

   class [[eosio::contract("exchanger")]] exchanger : public eosio::contract {
      public:
         using contract::contract;

         [[eosio::action]]
         void createoffer(const name& account, const name& offer_type, const asset& quantity, const double& price);

         [[eosio::action]]
         void canceloffer(const name& account, const name& offer_type);

         [[eosio::action]]
         void executeoffer(const name& account, const name& offer_type, const name& seller, const asset& quantity);

         [[eosio::action]]
         void withdraw(const name& account, const asset& quantity);

         [[eosio::on_notify("rem.token::transfer")]]
         void deposit(const name& account, const name& exchanger_contract, const asset& quantity, const string& memo);
      private:
         struct [[eosio::table]] account {
            asset    balance;
            bool     is_locked;

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

            // explicit serialization macro is not necessary, used here only to improve compilation time
            EOSLIB_SERIALIZE( account, (balance)(is_locked) )
         };

         static constexpr symbol sys_symbol{"SYS", 4};
         enum class offer_type : uint64_t { buy = "buy"_n.value, sell = "sell"_n.value };

         struct [[eosio::table]] offers {
               name              account;
               asset             quantity;
               double            price;
               block_timestamp   offer_timestamp;

               uint64_t primary_key() const { return account.value; }
               uint64_t by_amount() const { return quantity.amount; }
               double by_price() const { return price; }

               // explicit serialization macro is not necessary, used here only to improve compilation time
               EOSLIB_SERIALIZE( offers, (account)(quantity)(price)(offer_timestamp) )
            };

         typedef eosio::multi_index< "accounts"_n, account > accounts;
         typedef multi_index<"offers"_n, offers,
            indexed_by<"byamount"_n, const_mem_fun < offers, uint64_t, &offers::by_amount>>,
            indexed_by<"byprice"_n, const_mem_fun < offers, double, &offers::by_price>>
            > offers_idx;

         void add_balance(const name& account, const asset& value);
         void sub_balance(const name& account, const asset& value);
         void lock_wallet(const bool& lock_wallet, const name& account, const asset& value);
   };
}

add_balance method

In order to change the account balance, you need to determine the table and find this account. If the account is not found, then you need to create an entry in the table. If an account is not found and a new record is created, then such a token is not locked. In other cases, you just need to change the account balance on value.

void exchanger::add_balance(const name& account, const asset& value) {
   accounts acc_tbl( get_self(), account.value );
   auto acc_it = acc_tbl.find( value.symbol.code().raw() );
   if( acc_it == acc_tbl.end() ) {
      acc_tbl.emplace( get_self(), [&]( auto& a ){
         a.balance = value;
         a.is_locked = false;
      });
   } else {
      acc_tbl.modify( acc_it, same_payer, [&]( auto& a ) {
         a.balance += value;
      });
   }
}

sub_balance method

In the event of a decrease in the account balance, the record should already be in the table, if not, there must be a raise error.

Also, the amount deducted from the balance sheet must not exceed the balance sheet amount.

const std::string err_str = "no balance object found for: "s + account.to_string() + "; symbol: "s + value.symbol.code().to_string();
const auto& acc_it = acс_tbl.get( value.symbol.code().raw(), err_str.c_str() );
check( acc_it.balance.amount >= value.amount, "overdrawn balance" );

Together:

void exchanger::sub_balance(const name& account, const asset& value) {
   accounts acс_tbl( get_self(), account.value );

   const std::string err_str = "no balance object found for: "s + account.to_string() + "; symbol: "s + value.symbol.code().to_string();
   const auto& acc_it = acс_tbl.get( value.symbol.code().raw(), err_str.c_str() );
   check( acc_it.balance.amount >= value.amount, "overdrawn balance" );

   acс_tbl.modify( acc_it, get_self(), [&]( auto& a ) {
      a.balance -= value;
   });
}

lock_wallet method

The same situation as with sub_balance, the record should already be in the table, if not, there must be a raise error.

Method get will be raise error err_str if the account cannot be found:

const std::string err_str = "no balance object found for: "s + account.to_string() + "; symbol: "s + value.symbol.code().to_string();
const auto& account_it = acc_tbl.get( value.symbol.code().raw(), err_str.c_str() );
if variable lock_wallet=true then it will lock wallet, if lock_wallet=false it will unlock wallet.
Together:
void exchanger::lock_wallet(const bool& lock_wallet, const name& account, const asset& value) {
   accounts acc_tbl( get_self(), account.value );

   const std::string err_str = "no balance object found for: "s + account.to_string() + "; symbol: "s + value.symbol.code().to_string();
   const auto& account_it = acc_tbl.get( value.symbol.code().raw(), err_str.c_str() );

   acc_tbl.modify( account_it, get_self(), [&]( auto& a ) {
      a.is_locked = lock_wallet;
   });
}

Deposit action

Deposit function will be triggered by the rem.token by using on_notify attribute.

Note

As triggered contract is not necessary to be a rem.token it can be any other contract.

void exchanger::deposit(const name& account, const name& exchanger_contract, const asset& quantity, const string& memo) {
    if (exchanger_contract != get_self() || account == get_self()) {
        return;
    }
    add_balance(account, quantity);
}

Firstly, the action checks that the contract is not transferring to itself:

if (exchanger_contract != get_self() || account == get_self()) {
    return;
}

The contract needs to do so because transferring to the contract account itself would create an invalid booking situation in which an account could have more tokens than the account has in the rem.token contract.

Then, we should change account virtual balance:

add_balance(account, quantity);

Include external contracts

Let’s include the rem.token and rem.system contracts, to use transfer_action wrap and get core symbol from rem.system by method system_contract::get_core_symbol().

Firstly, we need to clone rem.contracts to exchanger contract directory:

$ git clone https://github.com/Remmeauth/rem.contracts.git

Then, so that the exchanger contract can include rem.token and rem.system contracts need to change CmakeLists file.

Let’s add the location of the rem.token and rem.system contracts to src/CmakeLists.txt:

target_include_directories( exchanger
        PUBLIC
        ${CMAKE_SOURCE_DIR}/../include
        ${CMAKE_SOURCE_DIR}/../rem.contracts/contracts/rem.token/include
        ${CMAKE_SOURCE_DIR}/../rem.contracts/contracts/rem.system/include
        )

As a result, the CmakeLists.txt file will look something like this:

project(exchanger)

set(EOSIO_WASM_OLD_BEHAVIOR "Off")
find_package(eosio.cdt)

add_contract( exchanger exchanger exchanger.cpp )
target_include_directories( exchanger
        PUBLIC
        ${CMAKE_SOURCE_DIR}/../include
        ${CMAKE_SOURCE_DIR}/../rem.contracts/contracts/rem.token/include
        ${CMAKE_SOURCE_DIR}/../rem.contracts/contracts/rem.system/include
        )
target_ricardian_directory( exchanger ${CMAKE_SOURCE_DIR}/../ricardian )

Now, we can include rem.token and rem.system to exchanger.cpp.

#include <exchanger.hpp>
#include <rem.system/rem.system.hpp>
#include <rem.token/rem.token.hpp>

Tip

You can add rem.contracts as git submodule. More information about git submodules.

Withdraw action

void exchanger::withdraw(const name& account, const asset& quantity) {
   require_auth(account);
   accounts acc_tbl( get_self(), account.value );
   const std::string err_str = "no balance object found for: "s + account.to_string() + "; symbol: "s + quantity.symbol.code().to_string();
   const auto& acc_it = acc_tbl.get( quantity.symbol.code().raw(), err_str.c_str() );

   check( !acc_it.is_locked, "at first, cancel open offers" );
   check( quantity.is_valid(), "invalid quantity" );
   check( quantity.amount > 0, "must transfer positive quantity" );

   sub_balance(account, quantity);

   token::transfer_action transfer(system_contract::token_account, {get_self(), system_contract::active_permission});
   transfer.send(get_self(), account, quantity, "withdrawal of funds from the exchanger");
}

First, check the authority of the account

require_auth(account);
To withdraw funds, an entry in the table must already exist.
Let’s define a table and find an account:
accounts acc_tbl( get_self(), account.value );
const std::string err_str = "no balance object found for: "s + account.to_string() + "; symbol: "s + quantity.symbol.code().to_string();
const auto& acc_it = acc_tbl.get( quantity.symbol.code().raw(), err_str.c_str() );

Now, we can check:

  • Is the wallet locked
  • Is quantity valid
  • Is quantity a positive value
check( !acc_it.is_locked, "at first, cancel open offers" );
check( quantity.is_valid(), "invalid quantity" );
check( quantity.amount > 0, "must transfer positive quantity" );

Then, we will reduce the funds from the virtual wallet balance:

sub_balance(account, quantity);
Now, let’s use the rem.token contract. We use token::transfer_action wrap to send transfer action to the network.
The first argument is the name of the contract that uses the transfer action, second argument is authority of account which is used to sign the action.

Authorization consists of:

  • Account name
  • Account permission
token::transfer_action transfer(system_contract::token_account, {get_self(), system_contract::active_permission});
transfer.send(get_self(), account, quantity, "withdrawal of funds from the exchanger");

Here we used the exchanger account name as account name and active permission that declared in rem.system contract.

Createoffer action

As the creator of the offer, I can either buy or sell. For example, REM is a core symbol, then, if I want to buy REM, I create an offer with type buy and quantity with symbol REM payment currency will be SYS, otherwise, if selling REM, I create offer with type sell and quantity with symbol REM in return i will get quantity with symbol SYS.

void exchanger::createoffer(const name& account, const name& offer_type, const asset& quantity, const double& price) {
   require_auth(account);

   check(offer_type.value == static_cast<uint64_t>( offer_type::buy ) || offer_type.value == static_cast<uint64_t>( offer_type::sell ), "unsupported offer type");
   check( quantity.is_valid(), "invalid quantity" );
   check( quantity.amount > 0, "quantity must be positive value" );
   check( price > 0, "price should be a positive value" );
   check( quantity.symbol == system_contract::get_core_symbol(), "symbol mismatch" );

   asset payable_value = offer_type.value == static_cast<uint64_t>( offer_type::buy ) ? asset{int64_t(quantity.amount * price), sys_symbol}: quantity;
   const string err_str = "no balance object found for: "s + account.to_string() + "; symbol: "s + payable_value.symbol.code().to_string();
   accounts account_tbl(get_self(), account.value);
   const auto& account_it = account_tbl.get( payable_value.symbol.code().raw(), err_str.c_str() );

   check( account_it.balance.amount >= payable_value.amount, "overdrawn balance" );
   check( payable_value.amount > 0, "amount less than the minimum deal offer );

   uint64_t offer_scope = offer_type.value == static_cast<uint64_t>( offer_type::buy ) ? static_cast<uint64_t>( offer_type::sell ) : static_cast<uint64_t>( offer_type::buy );
   offers_idx offers_tbl(get_self(), offer_scope);
   check(offers_tbl.find(account.value) == offers_tbl.end(), "to create a new offer, first, remove the old");

   offers_tbl.emplace(get_self(), [&](auto &o) {
      o.account         = account;
      o.quantity        = quantity;
      o.price           = price;
      o.offer_timestamp = current_time_point();
   });
   lock_wallet(true, account, payable_value);
}

At first, check the authority of the account

require_auth(account);

Then, we need to check that the arguments are valid:

  • is valid offer_type
  • is valid quantity
  • is positive quantity and price values
  • is an exchange symbol a core_symbol
check(offer_type.value == static_cast<uint64_t>( offer_type::buy ) || offer_type.value == static_cast<uint64_t>( offer_type::sell ), "unsupported offer type");
check( quantity.is_valid(), "invalid quantity" );
check( quantity.amount > 0, "quantity must be positive value" );
check( price > 0, "price should be a positive value" );
check( quantity.symbol == system_contract::get_core_symbol(), "symbol mismatch" );

Then, if offer type is buy, the payable_value will be SYS, if offer type sell it will be REM.

asset payable_value = offer_type.value == static_cast<uint64_t>( offer_type::buy ) ? asset{int64_t(quantity.amount * price), sys_symbol}: quantity;

Check if there are funds on the virtual wallet of the account to buy or sell and if there are enough of them.

const string err_str = "no balance object found for: "s + account.to_string() + "; symbol: "s + payable_value.symbol.code().to_string();
accounts account_tbl(get_self(), account.value);
const auto& account_it = account_tbl.get( payable_value.symbol.code().raw(), err_str.c_str() );

check( account_it.balance.amount >= payable_value.amount, "overdrawn balance" );
check( payable_value.amount > 0, "amount less than the minimum deal offer" );

Now, let`s define the table for the offer. If offer type is buy, account wants to buy REM, then, executor of the offer wants to sell them REM, the executor needs to find an offer in exchange_contract sell offers table, and vice versa.

uint64_t offer_scope = offer_type.value == static_cast<uint64_t>( offer_type::buy ) ? static_cast<uint64_t>( offer_type::sell ) : static_cast<uint64_t>( offer_type::buy );

One account can place a maximum of 1 buy and sell offer. Let’s define a table offers and add new one:

offers_idx offers_tbl(get_self(), offer_scope);
check(offers_tbl.find(account.value) == offers_tbl.end(), "to create a new offer, first, remove the old");

offers_tbl.emplace(get_self(), [&](auto &o) {
   o.account         = account;
   o.quantity        = quantity;
   o.price           = price;
   o.offer_timestamp = current_time_point();
});

Now, so that the account does not spend paying currency, let’s lock it:

lock_wallet(true, account, payable_value);

Canceloffer action

void exchanger::canceloffer(const name& account, const name& offer_type) {
   require_auth(account);
   check(offer_type.value == static_cast<uint64_t>( offer_type::buy ) || offer_type.value == static_cast<uint64_t>( offer_type::sell ), "unsupported offer type");

   uint64_t offer_scope = offer_type.value == static_cast<uint64_t>( offer_type::buy ) ? static_cast<uint64_t>( offer_type::sell ) : static_cast<uint64_t>( offer_type::buy );
   offers_idx offers_tbl(get_self(), offer_scope);
   auto offer_it = offers_tbl.find(account.value);
   check(offer_it != offers_tbl.end(), "an order does not exist");

   asset payable_value = offer_type.value == static_cast<uint64_t>( offer_type::buy ) ? asset{int64_t(offer_it->quantity.amount * offer_it->price), sys_symbol}: offer_it->quantity;
   offers_tbl.erase(offer_it);
   lock_wallet(false, account, payable_value);
}

At first, check the authority of account and valid offer type

require_auth(account);
check(offer_type.value == static_cast<uint64_t>( offer_type::buy ) || offer_type.value == static_cast<uint64_t>( offer_type::sell ), "unsupported offer type");

We have already explained the meaning of the offer_scope variable. Let’s define a table and make sure that such an offer is in the table offers:

uint64_t offer_scope = offer_type.value == static_cast<uint64_t>( offer_type::buy ) ? static_cast<uint64_t>( offer_type::sell ) : static_cast<uint64_t>( offer_type::buy );
offers_idx offers_tbl(get_self(), offer_scope);
auto offer_it = offers_tbl.find(account.value);
check(offer_it != offers_tbl.end(), "an order does not exist");

Define a paid currency to unlock a virtual wallet:

asset payable_value = offer_type.value == static_cast<uint64_t>( offer_type::buy ) ? asset{int64_t(offer_it->quantity.amount * offer_it->price), sys_symbol}: offer_it->quantity;

Then, delete the offer and unlock virtual wallet for payable_value

offers_tbl.erase(offer_it);
lock_wallet(false, account, payable_value);

Executeoffer action

void exchanger::executeoffer(const name& account, const name& offer_type, const name& seller, const asset& quantity) {
   require_auth(account);
   check( quantity.is_valid(), "invalid quantity" );
   check( quantity.amount > 0, "quantity must be positive value" );
   check( quantity.symbol == system_contract::get_core_symbol(), "symbol mismatch" );
   check(offer_type.value == static_cast<uint64_t>( offer_type::buy ) || offer_type.value == static_cast<uint64_t>( offer_type::sell ), "unsupported offer type");

   offers_idx offers_tbl(get_self(), offer_type.value);
   auto offer_it = offers_tbl.find(seller.value);

   check(offer_it != offers_tbl.end(), "an order does not exist");
   check(offer_it->quantity.amount >= quantity.amount, "the order amount is less than the purchase amount");

   asset payable_value = offer_type.value == static_cast<uint64_t>( offer_type::buy ) ? asset{int64_t(offer_it->quantity.amount * offer_it->price), sys_symbol}: offer_it->quantity;
   asset received_value = offer_type.value == static_cast<uint64_t>( offer_type::buy ) ? offer_it->quantity : asset{int64_t(offer_it->quantity.amount * offer_it->price), sys_symbol};
   const string err_str = "no balance object found for: "s + account.to_string() + "; symbol: "s + payable_value.symbol.code().to_string();
   accounts account_tbl(get_self(), account.value);
   const auto& account_it = account_tbl.get( payable_value.symbol.code().raw(), err_str.c_str() );

   check( payable_value.amount > 0, "amount less than the minimum deal offer" );
   check( !account_it.is_locked, "at first, cancel open orders" );
   check( account_it.balance.amount >= payable_value.amount, "overdrawn balance" );

   if (offer_it->quantity.amount != quantity.amount) {
      offers_tbl.modify( offer_it, get_self(), [&]( auto& o ) {
         o.quantity -= quantity;
      });
   } else {
      offers_tbl.erase(offer_it);
      lock_wallet(false, seller, received_value);
   }

   sub_balance(account, payable_value);
   add_balance(account, received_value);

   sub_balance(seller, received_value);
   add_balance(seller, payable_value);
}

At first, check the authority of account and is valid offer type and quantity:

require_auth(account);
check( quantity.is_valid(), "invalid quantity" );
check( quantity.amount > 0, "quantity must be positive value" );
check( quantity.symbol == system_contract::get_core_symbol(), "symbol mismatch" );
check(offer_type.value == static_cast<uint64_t>( offer_type::buy ) || offer_type.value == static_cast<uint64_t>( offer_type::sell ), "unsupported offer type");

As an executor of an offer, I can fulfill it in whole or in part, but not more. Let’s find our offer and check it out:

offers_idx offers_tbl(get_self(), offer_type.value);
auto offer_it = offers_tbl.find(seller.value);

check(offer_it != offers_tbl.end(), "an order does not exist");
check(offer_it->quantity.amount >= quantity.amount, "the order amount is less than the purchase amount");

Then, define the payable_value and received_value for executor. If I, as an executor, fulfill a sales REM offer, I will have to pay SYS and vice versa. As an executor, if I need to buy REM, I need to find seller in table exchanger buy offers.

asset payable_value = offer_type.value == static_cast<uint64_t>( offer_type::buy ) ? asset{int64_t(offer_it->quantity.amount * offer_it->price), sys_symbol}: offer_it->quantity;
asset received_value = offer_type.value == static_cast<uint64_t>( offer_type::buy ) ? offer_it->quantity : asset{int64_t(offer_it->quantity.amount * offer_it->price), sys_symbol};

Now, check that the account has enough funds in the virtual wallet to pay and is not locked:

const string err_str = "no balance object found for: "s + account.to_string() + "; symbol: "s + payable_value.symbol.code().to_string();
accounts account_tbl(get_self(), account.value);
const auto& account_it = account_tbl.get( payable_value.symbol.code().raw(), err_str.c_str() );

check( payable_value.amount > 0, "amount less than the minimum deal offer" );
check( !account_it.is_locked, "at first, cancel open orders" );
check( account_it.balance.amount >= payable_value.amount, "overdrawn balance" );

Then, in the case of an offer for the full amount the offer will be deleted and the seller’s wallet will be unlocked, otherwise, open offer amount will be reduced.

if (offer_it->quantity.amount != quantity.amount) {
   offers_tbl.modify( offer_it, get_self(), [&]( auto& o ) {
      o.quantity -= quantity;
   });
} else {
   offers_tbl.erase(offer_it);
   lock_wallet(false, seller, received_value);
}

Now we can deduct payable_value from executor account and increase received_value on virtual wallet, and deduct received_value from seller and increase their payable_value:

sub_balance(account, payable_value);
add_balance(account, received_value);

sub_balance(seller, received_value);
add_balance(seller, payable_value);

Now the file exchanger.cpp will look like this:

#include <exchanger.hpp>
#include <rem.system/rem.system.hpp>
#include <rem.token/rem.token.hpp>

namespace exchanger_contract {

using eosiosystem::system_contract;

void exchanger::createoffer(const name& account, const name& offer_type, const asset& quantity, const double& price) {
   require_auth(account);

   check(offer_type.value == static_cast<uint64_t>( offer_type::buy ) || offer_type.value == static_cast<uint64_t>( offer_type::sell ), "unsupported offer type");
   check( quantity.is_valid(), "invalid quantity" );
   check( quantity.amount > 0, "quantity must be positive value" );
   check( price > 0, "price should be a positive value" );
   check( quantity.symbol == system_contract::get_core_symbol(), "symbol mismatch" );

   asset payable_value = offer_type.value == static_cast<uint64_t>( offer_type::buy ) ? asset{int64_t(quantity.amount * price), sys_symbol}: quantity;
   const string err_str = "no balance object found for: "s + account.to_string() + "; symbol: "s + payable_value.symbol.code().to_string();
   accounts account_tbl(get_self(), account.value);
   const auto& account_it = account_tbl.get( payable_value.symbol.code().raw(), err_str.c_str() );

   check( account_it.balance.amount >= payable_value.amount, "overdrawn balance" );
   check( payable_value.amount > 0, "amount less than the minimum deal offer" );

   uint64_t offer_scope = offer_type.value == static_cast<uint64_t>( offer_type::buy ) ? static_cast<uint64_t>( offer_type::sell ) : static_cast<uint64_t>( offer_type::buy );
   offers_idx offers_tbl(get_self(), offer_scope);
   check(offers_tbl.find(account.value) == offers_tbl.end(), "to create a new offer, first, remove the old");

   offers_tbl.emplace(get_self(), [&](auto &o) {
      o.account         = account;
      o.quantity        = quantity;
      o.price           = price;
      o.offer_timestamp = current_time_point();
   });
   lock_wallet(true, account, payable_value);
}

void exchanger::canceloffer(const name& account, const name& offer_type) {
   require_auth(account);
   check(offer_type.value == static_cast<uint64_t>( offer_type::buy ) || offer_type.value == static_cast<uint64_t>( offer_type::sell ), "unsupported offer type");

   uint64_t offer_scope = offer_type.value == static_cast<uint64_t>( offer_type::buy ) ? static_cast<uint64_t>( offer_type::sell ) : static_cast<uint64_t>( offer_type::buy );
   offers_idx offers_tbl(get_self(), offer_scope);
   auto offer_it = offers_tbl.find(account.value);
   check(offer_it != offers_tbl.end(), "an order does not exist");

   asset payable_value = offer_type.value == static_cast<uint64_t>( offer_type::buy ) ? asset{int64_t(offer_it->quantity.amount * offer_it->price), sys_symbol}: offer_it->quantity;
   offers_tbl.erase(offer_it);
   lock_wallet(false, account, payable_value);
}

void exchanger::executeoffer(const name& account, const name& offer_type, const name& seller, const asset& quantity) {
   require_auth(account);
   check( quantity.is_valid(), "invalid quantity" );
   check( quantity.amount > 0, "quantity must be positive value" );
   check( quantity.symbol == system_contract::get_core_symbol(), "symbol mismatch" );
   check(offer_type.value == static_cast<uint64_t>( offer_type::buy ) || offer_type.value == static_cast<uint64_t>( offer_type::sell ), "unsupported offer type");

   offers_idx offers_tbl(get_self(), offer_type.value);
   auto offer_it = offers_tbl.find(seller.value);

   check(offer_it != offers_tbl.end(), "an order does not exist");
   check(offer_it->quantity.amount >= quantity.amount, "the order amount is less than the purchase amount");

   asset payable_value = offer_type.value == static_cast<uint64_t>( offer_type::buy ) ? asset{int64_t(offer_it->quantity.amount * offer_it->price), sys_symbol}: offer_it->quantity;
   asset received_value = offer_type.value == static_cast<uint64_t>( offer_type::buy ) ? offer_it->quantity : asset{int64_t(offer_it->quantity.amount * offer_it->price), sys_symbol};
   const string err_str = "no balance object found for: "s + account.to_string() + "; symbol: "s + payable_value.symbol.code().to_string();
   accounts account_tbl(get_self(), account.value);
   const auto& account_it = account_tbl.get( payable_value.symbol.code().raw(), err_str.c_str() );

   check( payable_value.amount > 0, "amount less than the minimum deal offer" );
   check( !account_it.is_locked, "at first, cancel open orders" );
   check( account_it.balance.amount >= payable_value.amount, "overdrawn balance" );

   if (offer_it->quantity.amount != quantity.amount) {
      offers_tbl.modify( offer_it, get_self(), [&]( auto& o ) {
         o.quantity -= quantity;
      });
   } else {
      offers_tbl.erase(offer_it);
      lock_wallet(false, seller, received_value);
   }

   sub_balance(account, payable_value);
   add_balance(account, received_value);

   sub_balance(seller, received_value);
   add_balance(seller, payable_value);
}

void exchanger::deposit(const name& account, const name& exchanger_contract, const asset& quantity, const string& memo) {
   if (exchanger_contract != get_self() || account == get_self()) {
      return;
   }
   add_balance(account, quantity);
}

void exchanger::withdraw(const name& account, const asset& quantity) {
   require_auth(account);
   accounts acc_tbl( get_self(), account.value );
   auto acc_it = acc_tbl.find( quantity.symbol.code().raw() );
   check( !acc_it->is_locked, "at first, cancel open offers" );

   check( quantity.is_valid(), "invalid quantity" );
   check( quantity.amount > 0, "must transfer positive quantity" );

   sub_balance(account, quantity);

   token::transfer_action transfer(system_contract::token_account, {get_self(), system_contract::active_permission});
   transfer.send(get_self(), account, quantity, "withdrawal of funds from the exchanger");
}

void exchanger::add_balance( const name& account, const asset& value ) {
   accounts acc_tbl( get_self(), account.value );
   auto acc_it = acc_tbl.find( value.symbol.code().raw() );
   if( acc_it == acc_tbl.end() ) {
      acc_tbl.emplace( get_self(), [&]( auto& a ){
         a.balance = value;
         a.is_locked = false;
      });
   } else {
      acc_tbl.modify( acc_it, same_payer, [&]( auto& a ) {
         a.balance += value;
      });
   }
}

void exchanger::sub_balance( const name& account, const asset& value ) {
   accounts acс_tbl( get_self(), account.value );

   const std::string err_str = "no balance object found for: "s + account.to_string() + "; symbol: "s + value.symbol.code().to_string();
   const auto& acc_it = acс_tbl.get( value.symbol.code().raw(), err_str.c_str() );
   check( acc_it.balance.amount >= value.amount, "overdrawn balance" );

   acс_tbl.modify( acc_it, get_self(), [&]( auto& a ) {
      a.balance -= value;
   });
}

void exchanger::lock_wallet( const bool& lock_wallet, const name& account, const asset& value ) {
   accounts acc_tbl( get_self(), account.value );

   const std::string err_str = "no balance object found for: "s + account.to_string() + "; symbol: "s + value.symbol.code().to_string();
   const auto& account_it = acc_tbl.get( value.symbol.code().raw(), err_str.c_str() );

   acc_tbl.modify( account_it, get_self(), [&]( auto& a ) {
      a.is_locked = lock_wallet;
   });
}
}

Build exchange contract

To compile our contract go to the root of the exchanger directory, then:

$ cd build && cmake ..

Then, run make in build directory:

$ make

It will return something like:

[ 11%] Performing build step for 'exchanger_project'
Scanning dependencies of target exchanger
[ 50%] Building CXX object CMakeFiles/exchanger.dir/exchanger.obj
Warning, empty ricardian clause file
Warning, action <createoffer> does not have a ricardian contract
Warning, action <canceloffer> does not have a ricardian contract
Warning, action <executeoffer> does not have a ricardian contract
Warning, action <withdraw> does not have a ricardian contract
Warning, action <createoffer> does not have a ricardian contract
Warning, action <canceloffer> does not have a ricardian contract
Warning, action <executeoffer> does not have a ricardian contract
Warning, action <withdraw> does not have a ricardian contract
[100%] Linking CXX executable exchanger.wasm
[100%] Built target exchanger
[ 22%] No install step for 'exchanger_project'
[ 33%] No test step for 'exchanger_project'
[ 44%] Completed 'exchanger_project'
[100%] Built target exchanger_project