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.
Nice post! Steemit really needs syntax highlighting though :) Any thoughts on using something like AssemblyScript for writing contracts?
Great Suggestion
Can you compare with TurboScript? Looks like AssemblyScript is much more mature than TurboScript. I wonder though why we have two similar projects.
Does it decompile from wasm too?
AFAIK AssemblyScript was born out of dissatisfaction with some of the design choices in TurboScript. Not sure what the exact differences are anymore, looks like TurboScript is moving to binaryen now as well. I would personally go with AssemblyScript in a heartbeat just because I know that the dcode guy makes some great modules.
It doesn't decompile from wasm, and I don't think it ever will. You could maybe use SourceMaps to go back when support for that is added.
Hallo @dantheman ,
I have some questions about EOS:
Is it an operative system or only smart contracts?
Which languages do I need to learn in dept to be able to program a serious ride sharing service to take over the world of ride-sharing (I have programmed since I was a child, but as an adult mostly c# with asp.net framework, but also many other languages, mostly as hacks, never understood all the code)
I want to prepare myself to posibily make a ride-sharing app that out-compete uber and conventional taxi services, other then working as an economist (with programming jobs included in the job), I have been working as a taxitemp and a uber driver and know how to make a decentral solution that will solve all (or the crusial) problems of centralized ride/taxi services.
Mostly it is about sharing the 20-50% that uber or taxi company takes between the rider and the driver and getting all ratings on the blockchain. Uber manipulate ratings to their own benefit, and extort drivers... plus their business model is based on massive loans from the corrupt fiat central banks... I could go on with this, but back to the programming part.
Is it c++ that I need to master completely and/or is it realisticly for a semi-pro freelance soft programmer to learn before EOS is ready?
If it is a smart contract platform, then is the website or phone app still hosted on centralized serviecs?? You know it is all about getting that decentralized too, so that no regulation will be able to stop the app/site ??.. I have heard that steemit would run on EOS, but is that also the site itself? Maybe some of these questions shows how little I still know about EOS and other things and hope my questions are not too stupid :)
Thanks for the great work, I really enjoy Steem and also hold a little Bitshares and EOS.
Lasse
@dantheman Please answer this questions.
This is something that concerns me too... Could someone please address this?
@dantheman?
I spent some time learning Wren. Now I'll spend some time learning C++. I've done things in many different languages, but never C or C++. I'm excited for the journey, but also a bit overwhelmed thinking how far I have to go to get to where I'd someday like to be. Thank you for these examples.
Can you give us an idea when a testnet will be up so noobie muggles like myself can start playing around with this stuff?
C was my favorite for a long time, then I moved onto C++ but it still allowed me to do C so I didn't really benefit from C++ as much as I should have since it didn't FORCE me to learn the new things. More recently I've mostly been using C# which is similar in many ways but alien in others. :)
Hey you guys both inspired me. I'm reading your comments thinking, "Damn, I haven't tried programming anything in C or C++ since the early 2000s." I want to be able to make apps and develop ideas in modern ways. I've just been telling myself it's too hard or I'm never going to master it or ever even get close. That's limiting talk and I'm thinking now that I can make programs and learn to code if that's what I really want.
Very good "documentation", thanks @dan
@dan ... hard work there!! I know something about... Thank You!!
Oooo really very interesting post you I am very amazed to see him congratulations on the achievement that you can not all this apart from the results of your hard work in esteem I also want as you want kah you follow me and upvote me and share my post so that your friends can Even though not as good as your post if you want me very grateful to you Congratulations, hopefully sukse always make you up to the front
I don't code but somehow I find these post.... calming and exciting at the same time :-)
As I probably wouldn't write my own smart contracts but just use EOS (I wish it lots of success as the things I have read about it sound great) I don't mind if it is C++. Coming from a web development background and seeing the rapid adoption of JavaScript / NodeJS (Go too) for all kinds of things I think it's a bit of a pity that all those developers able to code would have a steep (ok maybe not steep but still) learning curve writing a contract using C++.
@dan Would there be the possibility to define smart contracts in ES6 and then (somehow) convert / compile them to C++?
JavaScript doesn't have declarative syntax like in C++. there is no all let's make this a variable an integer or this variable something else, whereas C++ does so. C++ also has classes and operator overloading whereas Javascript does not.
It seems to me C++ should be the soirce and JS sometimes the target. Although sometimes reading erors related to templates can be like reading Greek.
Good post thanks for sharing
This comment has received a 0.15 % upvote from @booster thanks to: @hamzaoui.
I don't know C++ but your explanation makes a lot of sense. Glad to hear about the progress on EOS.
Hi Dan. I am interested in the EOS project, and I wanted to ask you if you would like me to translate some of your post for the Spanish-speaking community, since I do not see a post on this topic and I think that in my community most people do not know EOS.
I am not a professional translator, but I would try to do my best.
I hope you have a good Friday!