(Part 1) Decentralized Exchange - Smart Contract API, Order Book And Orders (PT 1)

in #utopian-io5 years ago

Repository

https://github.com/igormuba/DEX/tree/master/Class1

What Will I Learn?

  • Working with contract APIs on the exchange
  • Deposit third party tokens to the exchange using the API
  • Withdraw tokens to an user using the API
  • Exchange order data structure and hierarchy

Requirements

  • Internet connection
  • Code editor
  • Browser

Difficulty

  • Intermediate

Tutorial Contents

On the previous tutorials I have covered lightly about a few security issues with smart contracts. And the last tutorial on the main Ethereum series, in special, I have covered Smart Contract APIs.

The last tutorials were planned to "prepare" the readers to get started on building a decentralized exchange on the Ethereum blockchain. And this post, according to the roadmap I planned with, was supposed to get started on the data structures for the decentralized exchange.

However, based on the feedback from the last tutorial, I think it would be a good thing to start this one with a practical example of how we can use a smart contract API on our exchange, something we would have to do at some point. So, here you will find a practical example on working with external contracts using a local API. And I will introduce you to the main data structure to the exchange, the one that manages the book of orders (buyers and sellers).

The API

I am coding all this exercise on a single file, so we don't have to import the API, for simplicity.

Here is how my exchange.sol file looks like:

pragma solidity ^0.5.0;

//API to expose the tokens functions to our exchange
contract ERC20API{ //we are basing the API on the ERC20 pattern

//API function to check token allowances
    function allowance(address tokenOwner, address spender) public view returns (uint);

//API function to transfer FROM the exchange
    function transfer(address to, uint tokens) public returns (bool);

//API function to transfer TO the exchange
    function transferFrom(address from, address to, uint tokens) public returns (bool);
}

contract exchange{
//exchange code will go here
}

The API is here to expose the functions we may need to our contract.

Deposit and withdraw tokens

Now, let us really work with that API by coding the functions for our decentralized exchange to be able to deposit and withdraw third party tokens.

Start by creating the mapping to save the token balances of our users:

contract exchange{
mapping (address => mapping(address=>uint)) tokenBalance;
}

Now, we make the function that will be used by the user to deposit to the exchange. Actually, what this function does, and you will notice it, is that it does not actually "receives a deposit", but it actually withdraws from the user account on the third party contract, and sends the tokens to the exchange balance on the third party token!

Captura de Tela 20190125 às 20.42.46.png

So, the first function, the "deposit", looks like this:

function depositToken(address _token, uint _amount) public {
//loads the API with the address given
    ERC20API tokenLoaded = ERC20API(_token);
//requires that our contract has the permission to withdraw form the user on the Token
    require(tokenLoaded.allowance(msg.sender, address(this))>=_amount);
//requires that the transfer to our account succeeds
    require(tokenLoaded.transferFrom(msg.sender, address(this), _amount));
//adds balance of the token on the user balance
    tokenBalance[msg.sender][_token]+=_amount;
}

Of course, for this function to work, we need that the user has, previously, authorized, or "approved", our exchange as a spender for his funds.

Notice that we don't have to use the require keyword to verify that our balance did not overflow (though, you can, if you wish) because the ERC20 pattern expects that the token himself implements that function. Also, we are not checking if the user balance has overflown because if the exchange balance, that is supposed to have the same, or more, amount of tokens than the user, did not overflow, then obviously the user's balance didn't too. If the contract we are working with does not have overflow checkers, then the user has a much bigger problem for using that token than us and the token probably won't survive for long as their security must be poor...

Now, for the withdraw function, to send tokens from our exchange to the users wallet:

function withdrawToken(address _token, uint _amount) public {
//we will call the API functions to the address
    ERC20API tokenLoaded = ERC20API(_token);

//requires that after the operation the users balance won't be negative
    require(tokenBalance[msg.sender][_token]-_amount>=0);
//requires that the users balance won't overflow after
    require(tokenBalance[msg.sender][_token]-_amount<=tokenBalance[msg.sender][_token]);
//reduces the users balance first
    tokenBalance[msg.sender][_token]-=_amount;
//calls the API function to transfer on the tokens contract form exchange to user
    require(tokenLoaded.transfer(msg.sender, _amount));
}

Notice that now, on this function, we need to check for underflow, as there is a change that the balance of the user can underflow (giving the user unlimited withdraw limit) before we even make a call to the third party contract with the API.

Also, we reduce the users balance on our contract before we transfer to avoid a race condition to allow the user to do unlimited withdrawals in one block.

Deposit and Withdraw ETH

This one is a bit more simple, and you can see the difference of when we have to work with a contract API to make external calls, and when we don't, as Ethereum transaction functions are inherent to Solidity.

First, we make a mapping to keep track of the users balance:

mapping (address => uint) ethBalance;

And we make a fallback function to allow users to send Ethereum to the exchange:

function() external payable{
//requires that this won't overflow
    require(ethBalance[msg.sender]+msg.value>=ethBalance[msg.sender]);
//adds the balance to the user
    ethBalance[msg.sender]+=msg.value;
}

Simple enough. Now, the withdraw function has a few more complications, to ensure no underflow happens:L

function withdrawEth(uint _wei) public {
    require(ethBalance[msg.sender]-_wei>=0);
    require(ethBalance[msg.sender]-_wei<=ethBalance[msg.sender]);
    ethBalance[msg.sender]-=_wei;
    msg.sender.transfer(_wei);
}

Token listing

We will, not, see the structures we will need for the further tutorials from the top to the bottom. Starting with the token list, inside the exchange contract.

First, the tokens will be stored in a mapping:

mapping (address=>Token) tokenList;

And that is the "structure" in the top.

The Token are objects, or struct, with the following data:

struct Token{
//a mapping of price to orders on that price (more on that later)
    mapping (uint => OrderBook) buyBook;
//a pointer to the highest price on this book
    uint maxBuyPrice;
//pointer to the lowest price
    uint minBuyPrice;
//size of how many prices are there
    uint amountBuyPrices;

//pretty similar to the above but for the sell book
    mapping (uint => OrderBook) sellBook;
    uint minSellPrice;
    uint maxSellPrice;
    uint amountSellPrice;
}

We are, using a mapping, storing another data struct (an order book), to allow us to have multiple orders at the same price.

Order book

The order book is a structure to keep track of:

  • The next higher price
  • The previous lower price
  • Order of placed orders in this price

It is very similar to a linked list, if you have studied data structures in traditional languages.

The code for the order book is:

struct OrderBook{
//what the is next higher price
    uint higherPrice;
//what is the previous higher price
    uint lowerPrice;
        
//list of orders in this price
    mapping (uint => Offer) offers;
//pointer to the first order
    uint offerPointer;
//how many orders in this price
    uint offerLength;
}

And you might have noticed we introduced the Offer structure. That is the real order from the users.

User order

The order is the simplest data set, or the one in the "lowest" side of the data structure of the exchange.

It is as simple as:

struct Offer{
    uint amount;
    address maker;
}

Because we already have the price on the order book, the order(or offer) itself just needs to store the amount of tokens and who placed it.

How all those structures look like

Here is an illustration for batter understanding of how those structures above relate to each other:
Captura de Tela 20190125 às 21.34.11.png

In the illustration above, first, a seller would fill the orders of 30wei, after all offers priced at 30wei are filled, the system will fill starting from 17wei on the buy book, in order, from the first placed order to the last.

State machine

The code we have finished is the "state machine" of our exchange, or the "skeleton" we could say. What we did not is, we have set the data structure behind the exchange itself. We still have to implement logic for multiple sections of the exchange. Other than that, we have set the API and implemented the deposit and withdraw exchange functionalities, which are the base of how the exchange contract will work. Without tokens or Ethereum, there is not volume for the exchange to work.

The final code for the skeleton of the exchange, including the API looks like:

pragma solidity ^0.5.0;

contract ERC20API{
    function allowance(address tokenOwner, address spender) public view returns (uint);
    function transfer(address to, uint tokens) public returns (bool);
    function transferFrom(address from, address to, uint tokens) public returns (bool);
}

contract exchange{
    
    struct Offer{
        uint amount;
        address maker;
    }
    struct OrderBook{
        uint higherPrice;
        uint lowerPrice;
        
        mapping (uint => Offer) offers;
        uint offerPointer;
        uint offerLength;
    }
    struct Token{
        address tokenContract;
        mapping (uint => OrderBook) buyBook;
        uint maxBuyPrice;
        uint minBuyPrice;
        uint amountBuyPrices;
        
        mapping (uint => OrderBook) sellBook;
        uint minSellPrice;
        uint maxSellPrice;
        uint amountSellPrice;
    }
    
    mapping (address=>Token) tokenList;
    
    mapping (address => uint) ethBalance;
    
    mapping (address => mapping(address=>uint)) tokenBalance;
    
    function() external payable{
        require(ethBalance[msg.sender]+msg.value>=ethBalance[msg.sender]);
        ethBalance[msg.sender]+=msg.value;
    }
    
    function withdrawEth(uint _wei) public {
        require(ethBalance[msg.sender]-_wei>=0);
        require(ethBalance[msg.sender]-_wei<=ethBalance[msg.sender]);
        ethBalance[msg.sender]-=_wei;
        msg.sender.transfer(_wei);
    }
    
    function depositToken(address _token, uint _amount) public {
        ERC20API tokenLoaded = ERC20API(_token);
        require(tokenLoaded.allowance(msg.sender, address(this))>=_amount);
        require(tokenLoaded.transferFrom(msg.sender, address(this), _amount));
        tokenBalance[msg.sender][_token]+=_amount;
    }
    
    function withdrawToken(address _token, uint _amount) public {
        ERC20API tokenLoaded = ERC20API(_token);
        require(tokenBalance[msg.sender][_token]-_amount>=0);
        require(tokenBalance[msg.sender][_token]-_amount<=tokenBalance[msg.sender][_token]);
        tokenBalance[msg.sender][_token]-=_amount;
        require(tokenLoaded.transfer(msg.sender, _amount));
    }
 
}

On the next tutorials of the "decentralized exchange" series we will deploy many "fake" tokens on a local test-net, to simulate a real exchange that could work with any token, as long as it is ERC20 compatible.

Curriculum

First tutorial:

Latest tutorial (on smart contract APIs):

Other related/useful contracts for this series:

Beneficiaries

This post has as beneficiaries

using the SteemPeak beneficiary tool

Captura de Tela 20190122 às 16.07.11.png

Sort:  

Thank you for your contribution @igormuba.

  • Again to bring good content to your tutorials. Good job!

Your contribution has been evaluated according to Utopian policies and guidelines, as well as a predefined set of questions pertaining to the category.

To view those questions and the relevant answers related to your post, click here.


Need help? Chat with us on Discord.

[utopian-moderator]

Thank you for your review, @portugalcoin! Keep up the good work!

Hi @igormuba!

Your post was upvoted by @steem-ua, new Steem dApp, using UserAuthority for algorithmic post curation!
Your post is eligible for our upvote, thanks to our collaboration with @utopian-io!
Feel free to join our @steem-ua Discord server

Hey, @igormuba!

Thanks for contributing on Utopian.
We’re already looking forward to your next contribution!

Get higher incentives and support Utopian.io!
Simply set @utopian.pay as a 5% (or higher) payout beneficiary on your contribution post (via SteemPlus or Steeditor).

Want to chat? Join us on Discord https://discord.gg/h52nFrV.

Vote for Utopian Witness!

Coin Marketplace

STEEM 0.18
TRX 0.14
JST 0.030
BTC 59708.78
ETH 3185.76
USDT 1.00
SBD 2.45