Category: EOS (EOS)

EOS – Example Exchange Contract and Benefits of C++

This week I have been focused on the API that smart contract developers will use to write contracts. To help facilitate the design of this API I have given myself an example contract to write. This time the example is a little bit more complex than just a currency contract, but a full up exchange between the native EOS currency and an example CURRENCY contract.

Benefits of C++ API

Developers building on EOS will write their smart contracts in C++ that gets compiled to Web Assembly and then published to the blockchain. This means that we can take advantage of C++’s type and template system to ensure our contracts are safe.

One of the most basic kinds of safety is known as dimensional analysis, aka keeping your units straight. When developing an exchange you are dealing with several numbers with different units: EOS, CURRENCY, and EOS / CURRENCY.

A simple implementation would do something like this:

struct account {
  uint64_t  eos_balance;
  uint64_t  currency_balance;
};


The problem with this simple approach is that the following code could be accidentally written:

void buy( Bid order ) {
   ...
   buyer_account.currency _balance  -=  order.quantity;
   ...
}

At first glance the error isn’t obvious, but upon closer inspection bids consume the eos_balance rather than the currency_balance. In this case the market is setup to price CURRENCY in EOS. Another kind of error that could occur is the following:

   auto receive_tokens = order.quantity * order.price;

This particular line of code may be valid if order is a Bid, but the price would need to be inverted if it were an Ask. As you can see without proper dimensional analysis there is no way to be certain you are adding apples to apples and not apples to oranges.

Fortunately, C++ allows us to use templates and operator overloading to define a runtime cost-free validation of our units.

template<typename NumberType, uint64_t CurrencyType = N(eos) >
   struct token {
       token(){}
       explicit token( NumberType v ):quantity(v){};
       NumberType quantity = 0;

       token& operator-=( const token& a ) {
          assert( quantity >= a.quantity, 
                        "integer underflow subtracting token balance" );
          quantity -= a.quantity;
          return *this;
       }

       token& operator+=( const token& a ) {
          assert( quantity + a.quantity >= a.quantity, 
                       "integer overflow adding token balance" );
          quantity += a.quantity;
          return *this;
       }

       inline friend token operator+( const token& a, const token& b ) {
          token result = a;
          result += b;
          return result;
       }

       inline friend token operator-( const token& a, const token& b ) {
          token result = a;
          result -= b;
          return result;
       }

       explicit operator bool()const { return quantity != 0; }
   };

With this definition there is now a clear type distinction in the account:

struct Account {
   eos::Tokens               eos_balance;
   currency::Tokens    currency_balance;
};

struct Bid {
    eos::Tokens quantity;
};


With this in place the following will generate a compile error because there is no -= operator defined for eos::Tokens and currency::Tokens.

void buy( Bid order ) {
   ...
   buyer_account.currency _balance  -=  order.quantity;
   ...
}

Using this technique I was able to use the compiler to identify and fix many unit mismatches in my implementation of an example exchange contract. The really nice thing about all of this is that the final web assembly generated by the C++ compiler is identical to what would have been generated if I had simply used uint64_t for all of my balances.

Another thing you may notice is that the token class also automatically checks for over and underflow exceptions.

Simplified Currency Contract

In the process of writing the exchange contract I first had update the currency contract. In doing so I refactored the currency contract into a header currency.hpp and a source currency.cpp so that the exchange contract could access types defined by the currency contract.

currency.hpp

#include <eoslib/eos.hpp>
#include <eoslib/token.hpp>
#include <eoslib/db.hpp>

/**
 * Make it easy to change the account name the currency is deployed to.
 */
#ifndef TOKEN_NAME
#define TOKEN_NAME currency
#endif

namespace TOKEN_NAME {

   typedef eos::token<uint64_t,N(currency)> Tokens;

   /**
    *  Transfer requires that the sender and receiver be the first two
    *  accounts notified and that the sender has provided authorization.
    */
   struct Transfer {
      AccountName       from;
      AccountName       to;
      Tokens            quantity;
   };

   struct Account {
      Tokens           balance;

      bool  isEmpty()const  { return balance.quantity == 0; }
   };

   /**
    *  Accounts information for owner is stored:
    *
    *  owner/TOKEN_NAME/account/account -> Account
    *
    *  This API is made available for 3rd parties wanting read access to
    *  the users balance. If the account doesn't exist a default constructed
    *  account will be returned.
    */
   inline Account getAccount( AccountName owner ) {
      Account account;
      ///      scope, code, table,      key,       value
      Db::get( owner, N(currency), N(account), N(account), account );
      return account;
   }

} /// namespace TOKEN_NAME

currency.cpp

#include <currency/currency.hpp> /// defines transfer struct (abi)

namespace TOKEN_NAME {

   ///  When storing accounts, check for empty balance and remove account
   void storeAccount( AccountName account, const Account& a ) {
      if( a.isEmpty() ) {
         printi(account);
         ///        scope    table       key
         Db::remove( account, N(account), N(account) );
      } else {
         ///        scope    table       key         value
         Db::store( account, N(account), N(account), a );
      }
   }

   void apply_currency_transfer( const TOKEN_NAME::Transfer& transfer ) {
      requireNotice( transfer.to, transfer.from );
      requireAuth( transfer.from );

      auto from = getAccount( transfer.from );
      auto to   = getAccount( transfer.to );

      from.balance -= transfer.quantity; /// token subtraction has underflow assertion
      to.balance   += transfer.quantity; /// token addition has overflow assertion

      storeAccount( transfer.from, from );
      storeAccount( transfer.to, to );
   }

}  // namespace TOKEN_NAME


Introducing the Exchange Contract

The exchange contract processes the currency::Transfer and the eos::Transfer messages when ever the exchange is the sender or receiver. It also implements three of its own messages: buy, sell, and cancel. The exchange contract defines its public interface in exchange.hpp which is where the message types and database tables are defined.

exchange.hpp

#include <currency/currency.hpp>

namespace exchange {

   struct OrderID {
      AccountName name    = 0;
      uint64_t    number  = 0;
   };

   typedef eos::price<eos::Tokens,currency::Tokens>     Price;

   struct Bid {
      OrderID            buyer;
      Price              price;
      eos::Tokens        quantity;
      Time               expiration;
   };

   struct Ask {
      OrderID            seller;
      Price              price;
      currency::Tokens   quantity;
      Time               expiration;
   };

   struct Account {
      Account( AccountName o = AccountName() ):owner(o){}

      AccountName        owner;
      eos::Tokens        eos_balance;
      currency::Tokens   currency_balance;
      uint32_t           open_orders = 0;

      bool isEmpty()const { return ! ( bool(eos_balance) | bool(currency_balance) | open_orders); }
   };

   Account getAccount( AccountName owner ) {
      Account account(owner);
      Db::get( N(exchange), N(exchange), N(account), owner, account );
      return account;
   }

   TABLE2(Bids,exchange,exchange,bids,Bid,BidsById,OrderID,BidsByPrice,Price);
   TABLE2(Asks,exchange,exchange,bids,Ask,AsksById,OrderID,AsksByPrice,Price);


   struct BuyOrder : public Bid  { uint8_t fill_or_kill = false; };
   struct SellOrder : public Ask { uint8_t fill_or_kill = false; };
}

The exchange contract source code gets a bit long for this post, but you can view it on github. For now I will show the core message handler for a SellOrder to get an idea how it would be implemented:

void apply_exchange_sell( SellOrder order ) {
   Ask& ask = order;
   requireAuth( ask.seller.name );

   assert( ask.quantity > currency::Tokens(0), "invalid quantity" );
   assert( ask.expiration > now(), "order expired" );

   static Ask existing_ask;
   assert( AsksById::get( ask.seller, existing_ask ), "order with this id already exists" );

   auto seller_account = getAccount( ask.seller.name );
   seller_account.currency_balance -= ask.quantity;

   static Bid highest_bid;
   if( !BidsByPrice::back( highest_bid ) ) {
      assert( !order.fill_or_kill, "order not completely filled" );
      Asks::store( ask );
      save( seller_account );
      return;
   }

   auto buyer_account = getAccount( highest_bid.buyer.name );

   while( highest_bid.price >= ask.price ) {
      match( highest_bid, buyer_account, ask, seller_account );

      if( highest_bid.quantity == eos::Tokens(0) ) {
         save( seller_account );
         save( buyer_account );
         Bids::remove( highest_bid );
         if( !BidsByPrice::back( highest_bid ) ) {
            break;
         }
         buyer_account = getAccount( highest_bid.buyer.name );
      } else {
         break; // buyer's bid should be filled
      }
   }

   save( seller_account );
   if( ask.quantity ) {
      assert( !order.fill_or_kill, "order not completely filled" );
      Asks::store( ask );
   }
}

As you can see the code here is relatively concise, readable, safe, and most importantly performant.

Isn’t C++ an Unsafe Language?

Those who are into language wars may be familiar with the challenges that C and C++ programers have managing memory. Fortunately, most of those issues go away when building smart contracts because your program “restarts” with a clean slate at the start of every message. Furthermore, there is rarely a need to implement dynamic memory allocation. This entire exchange contract doesn’t call new or delete nor malloc or free. The WebAssembly framework will automatically reject any transaction that would address memory wrong.

This means that most of the problems with C++ go away when using it for short-lived message handlers and we are left with its many benefits.

Conclusion

EOS.IO software is progressing nicely and I couldn’t be happier with how much fun it is to write smart contracts using this API.