(Part 10) Decentralized Exchange - The Complete Buy Function And Working Exchange!(PT 10)

in utopian-io •  7 days ago

Repository

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

What Will I Learn?

  • Filling orders of the same price
  • Filling order of different price (respecting the limit sell order)
  • Removing a link on the top of the linked list

Requirements

  • Internet connection.
  • Code editor.
  • Browser.

Difficulty

  • Advanced.

Tutorial Contents

Introduction

On the previous tutorial, we have implemented the logic for when the user makes a sell order, the smart contract to search on the buy book for orders that fill it. So, if you want to sell a token at a limit price, it will try to find buy orders that are either that price or higher. The sell function iterates through the buy book, from the most expensive order to the least one, filling the sell order. When the sell order finds that the book has no more orders that are at least the required price by the seller, if there are remaining tokens, after filling the order from the compatible sell ones, the order is stored on the sell book.

Now it is time to reverse that and implement that on the opposite side of the book. The buy order will have a similar logic but reversed. The buyer "profits" if he can get cheaper tokens than his limit order, so we will go from the cheapest sell order to the most expensive. Also, here we will consume ETH for the order maker, not tokens.

At the end of the implementation, I will show how everything works together, how the functions behave and do the testings.

As said on the previous tutorial, the focus of this tutorial you are reading is to show the differences from the sell to the buy orders and to demonstrate how to use them and what are the effects. If you want to understand more in depth the logic behind it, the previous tutorial has more details on that.

The function

We are building this inside a function that was already coded into the contract. But so far, it only had the functionality to store a new order in case no order on the sell book matches our new one. This is the function:

function buyToken(address _token, uint _price, uint _amount) public{
//omitted code
}

Inside it, we had an if and else decision branch. We have already worked on the if, now is time for the else, that means, there are orders that can fill, even if just partially, the new order.

The outer loop

The outer loop iterates through the prices on the linked list of prices:

uint ethAmount = 0;
uint remainingAmount = _amount;
uint buyPrice = loadedToken.minSellPrice;
uint offerPointer;
while(buyPrice<=_price&&remainingAmount>0){
//outer loop, there will be an inner loop inside this
}

//calls the function again to store the remaining of the buy order
if (remainingAmount>0){
    buyToken(_token, _price, remainingAmount);
}

The inner loop

The following chunk of code is indeed the previous outer loop:

offerPointer = loadedToken.sellBook[buyPrice].offerPointer;
while(offerPointer <= loadedToken.sellBook[buyPrice].offerLength && remainingAmount >0){
    uint volumeAtPointer = loadedToken.sellBook[buyPrice].offers[offerPointer].amount;

    if(volumeAtPointer<=remainingAmount){
//if the stored order will be filled completely
    }else{
//if there will be something remaining not he stored sell order
    }

//if the queue at this price gets empty
    if(offerPointer==loadedToken.sellBook[buyPrice].offerLength
    &&
    loadedToken.sellBook[buyPrice].offers[offerPointer].amount==0){
        loadedToken.amountSellPrice--;
//if the buy book is empty
        if(buyPrice==loadedToken.sellBook[buyPrice].higherPrice
        ||
        loadedToken.sellBook[buyPrice].higherPrice==0){
//stores that the buy book is empty
            loadedToken.minSellPrice=0;
        }else{
//removes the current price as it is now empty of orders
            loadedToken.minSellPrice=loadedToken.sellBook[buyPrice].higherPrice;
            loadedToken.sellBook[loadedToken.sellBook[buyPrice].higherPrice].lowerPrice=0;
        }
    }
//moves the pointer up
    offerPointer++;
}
//updates the minimum buy price
buyPrice=loadedToken.minSellPrice;

Notice two key differences from the sell function:

  • We are going from the cheapest sell order to the least expensive
  • We remove the lowest price, not the highest

Because it is of the interest of the buyer to get the lowest price.

If the stored order will be filled completely

This is the first if statement, and here is the logic of what to do in case the stored sell order is completely filled by the new buy order (means, the sell is lower or equal to the buy):

//the eth moved on this operation
ethAmount=volumeAtPointer*buyPrice;
//requires that the buyer has the amount
require(ethBalance[msg.sender]>=ethAmount);
//requires the buyer won't underflow
require(ethBalance[msg.sender]-ethAmount<=ethBalance[msg.sender]);
//deducts the balance
ethBalance[msg.sender]-=ethAmount;
//gives the tokens to the seller
tokenBalance[msg.sender][_token]+=volumeAtPointer;
//cleans the sell order at the pointer
loadedToken.sellBook[buyPrice].offers[offerPointer].amount=0;
//adds the balance to the seller
ethBalance[loadedToken.sellBook[buyPrice].offers[offerPointer].maker]+=ethAmount;
//moves the pointer
loadedToken.sellBook[buyPrice].offerPointer++;
//updates how much the buyer still wants to buy
remainingAmount-=volumeAtPointer;

Notice above the security against the race conditions. We deduct the ETH balance from the buyer before doing any transaction. So if he tries to spam with many calls to this function, he won't have enough balance to exploit a possibly not updated balance.

If there will be something remaining not he stored sell order

//requires that we aren't doing this accidentally
require(loadedToken.sellBook[buyPrice].offers[offerPointer].amount>remainingAmount);
//the remaining from the buy is the smallest, so this defined the amount of ETH
ethAmount=remainingAmount*buyPrice;
//requires the buyer won't underflow
require(ethBalance[msg.sender]-ethAmount<=ethBalance[msg.sender]);
//deducts the ETH from the buyer
ethBalance[msg.sender]-=ethAmount;
//deducts the sell order
loadedToken.sellBook[buyPrice].offers[offerPointer].amount-=remainingAmount;
//gives the ETH to the seller
ethBalance[loadedToken.sellBook[buyPrice].offers[offerPointer].maker]+=ethAmount;
//gives the tokens to the buyer
tokenBalance[msg.sender][_token]+=remainingAmount;
//records that the order is completely filled
remainingAmount=0;

In case the order is not completely filled and all loops have ended, if there is something left that the buyer still has to buy, the function will call itself at the condition previously written:

if (remainingAmount>0){
    buyToken(_token, _price, remainingAmount);
}

And this will store a new buy order with the remaining.

Testing

To start the tests a simple exchange a very basic token were deployed on a virtual testing environment:
Captura de Tela 20190210 às 20.38.42.png

It is worth mentioning that, at this point, the contract is so complex that the deployment cost 4266979 gas.
Captura de Tela 20190210 às 20.39.46.png

According to a gas price calculator, that means a regular deployment would cost around 1.5 dollars, and a priority deployment would cost around 23 dollars of miner fees!

Captura de Tela 20190210 às 20.42.37.png

I have placed a few orders on both the buy and sell book. Let us do some tests to see them filling:
Captura de Tela 20190210 às 20.47.48.png

First, I sell 2 tokens at the price of 9:
Captura de Tela 20190210 às 20.49.04.png

As you see, this is what happened:

  1. the order at 10 was completely filled
  2. the order at 9 was partially filled

Now, I have placed an order to buy 10 tokens at the price of 25:
Captura de Tela 20190210 às 20.50.54.png

What happened is that all the orders up to 25 were filled, and the volume left was stored in a buy order at the price of 25.

So far, we pretty much have a working exchange, the contract can:

  • Manage ETH deposits
  • Manage ETH withdrawals
  • Manage Token deposits
  • Manage Token withdrawals
  • Execute trades
  • Store orders

But I need to make it clear: THE CONTRACT WE HAVE DEVELOP HAS LOW-SECURITY FEATURES

A real contract would take anywhere from weeks to months to execute all the security testings for all known exploits. So, do not use the contract from this class to release a real exchange. This is for educational purposes only.

Costs

You might have noticed it was very expensive to deploy this contract, from 1.5 dollars to over 20 dollars for the miner fees. And the calls to the contract are also very expensive, ranging from 2 cents (for very basic trades) up to 10 dollars and maybe more, depending on the size of the books!

A decentralized exchange is a mean of overcoming censorship and government control, but it has its price. It is not for people to transact small amounts, considering our exchange does not take fees and all the costs are just for the miners, people that trade huge volumes would benefit, because, even though a trade can cost 10 dollars, if someone uses the decentralized exchanger to do a million dollars trade, the fee would be negligible and much better than a centralized exchange!

Also, the new pattern that appears to be accepted as the best one, is to use the decentralized exchange as a settlement layer. Exchanges are investing in building their interfaces on a centralized server, and do a few trades on the decentralized version for settlement of assets, to cut down costs.

Curriculum

Latest tutorial of the series:

First tutorial of the series:

Beneficiaries

This post has as beneficiaries

using the SteemPeak beneficiary tool

Captura de Tela 20190122 às 16.07.11.png

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

Thank you for your contribution.

  • Another cool concept introduced today. Building DEX is definitely a nice thing to attempt.
  • I loved how you are now adding actual screenshots and examples, very helpful.
  • Also your security warning was well in place. Very important for anyone attempting to build a DEX.
  • There were few typos in your post. One header i could barely understand.

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, @mcfarhat! Keep up the good work!

This story was recommended by Steeve to its users and upvoted by one or more of them.

Check @steeveapp to learn more about Steeve, an AI-powered Steem interface.

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

Congratulations! Your post has been selected as a daily Steemit truffle! It is listed on rank 2 of all contributions awarded today. You can find the TOP DAILY TRUFFLE PICKS HERE.

I upvoted your contribution because to my mind your post is at least 7 SBD worth and should receive 95 votes. It's now up to the lovely Steemit community to make this come true.

I am TrufflePig, an Artificial Intelligence Bot that helps minnows and content curators using Machine Learning. If you are curious how I select content, you can find an explanation here!

Have a nice day and sincerely yours,
trufflepig
TrufflePig

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!