Project - DEX Final Code

YES, finally it works!! Thank you @dan-i, I can crack on and complete my project now.

2 Likes

Here’s my final code: https://github.com/msuscens/Smart-Contract-Programming-201/tree/master/DEX-Project

I developed it without looking at Fillip’s help videos, and I’m proud of my Dex contract code and in particular, my createMarketOrder code and functionality.

However, my market order tests would ideally be simplified by breaking them down into much smaller units. I tried to be thorough on each test, setting up the initial state, checking that initial state, saving pre-test variables, performing the test, and then checking the post-test variable states. Whilst I may have increased the strength of the tests, it has resulted in each test block being too large and complex (and a huge test file). I’d welcome any constructive suggestions on how I could improve my Test script style/approach?

I’ve read a little about before(), beforeEach(), after(), afterEach(), and was wondering if there’s any resource showing advanced use of these function blocks in practice?
Or perhaps there’s an advanced (professional) best practice guide/article on building thorough but concise test scripts (for TDD)?

I’d also appreciate any constructive feedback on improving my contract code further.

Finally, Fillip mentioned a list of other improvements that could be made to this Dex project - where is this list?

Overall a great course and project - I learned a lot!
Thank you.

1 Like

Hello everyone. So today i began working on the final market order assignment and again i wanted to develop my own algorithm and it was hard. Im 85% done an im creating this post to share some tips if figured out which made the whole process easer (A lot easier)

#1 Get out your Pen and paper
This tip may seem very old school but i tried just coding up and algorithm to resolve market orders on the fly developing it as i went and i quickily hit a dead end realising that my code was not going to be compaitible with a new feature i wanted to implemnt. This often happens when coding on the fly its a bad habbit.

So i decided to go back to square 1 and get out a sheet of paper and begin to develop an algorithm and input a few test cases to see how it preformed. This was very intresting because its amazing how much my algorithm develiped over the course of the day. Below is my first draft of my psudeo code which i rewrote in word instead of sharing my terrible handwriting

So just to explain it what i did was i created two instances of the order struct one fore the limit orders which we already had (holds buy/sell limits) and another for the market orders (holds by/sell limits). For my original idea i wanted to store the market order in seperate storage whereby when we add a market order then the algorithm runs through the limit order book and compares the amounts. I have 3 cases.

  • MarketOrder amount == LimitOrder amount

  • MarketOrder amount > LimitOrder amount

  • MarketOrder amount < LimitOrder amount

Baed on these conditions the token amounts are adjusted accordingly such that filled orders are realised if the amount for the order is equal to zero. Then we loop through both arrays and pop off 0 amount orders from each of the orderbook arrays. The psudeo code is below (note that this was my first draft and after coding it i had to make many more chnges
FirstAlgorithm

So this is the base version and i ran into many bugs which i was not able to realise through the pusdeoCode and this leads to my next tip you should do when developing algoritms

#Run specific tests line by line
Below is a one of dozen handwritten tests that i preformed on the above algorithm while trying to get it to work. Note that for readers this wont make any sense but i wanted to include it to show the process. So i kept getting a runtime error that i couldnt figure out from the error message in the console So i chose some input conditions and stepped through my algorithm step by step to see single out the location of the error. This helped me a as its easier to figure out what exactly your code is doing when you do these testy by hand. Again i concetred my test into a word doc for the purposes of this post
FirstAlgorithmTest
Eventually i got this algorithm working and handling all edge cases (only thing not included was transfers at this point). I was very happy but then i found a major flaw that made this compltely unusable. For the edge case of a very large limit order than requires multiple interations through the limit order book to filled caused the transaction to revert as my contract would run out of gas if more than 5 or so iterations were required.

The reason for this was the nested loops. They are to computationally expensive as the time complexity is now O(N^2) and will only be worse the more iterations we do. So i had to go back to the drawing board

#Trial and error helps immensley
So i neded to refactor my code and make it more efficient. Supprisingly because i already had a base idea of how i wanted to solve the limit order function from the above algorithm i quickily made the changes i needed to solve the problem quite efficently actually. Basically i changed my orderbook back to one order book so we would only need one loop. I added a boolean ‘filled’ property that is true if an order is settled. That way we can loop through and romove filled orders based on this filled attribute. There were a few other changes but the core functionality of my changed algorithm are the same. The modified algorithm is below
secondAlgorithm
This is the psudeo code and after tewaking my actual code this works very well. I decided to get the resolving of orders down first before doing the trasnfers because it broke down the whole proccess and made things easier. I stared do the token trasnfers and will fimish Tomorrow i am write my final tests in a js file to finalise everything

#Current state of code

function settleOrders(string memory ticker) public {

      Order[] storage LimitOrder = LimitOrderBook[ticker][uint(Side.BUY)];
      Order[] storage newOrder = LimitOrderBook[ticker][uint(Side.SELL)];

      for (uint i = 0; i < newOrder.length; i++) {
            
            //handle case when limit order book is empty
            if (LimitOrder.length == 0) {
              break;
            }
            //handle case when market order amount == limit order amt
            if (newOrder[i].amount == LimitOrder[i].amount) {
                uint fillAmount = newOrder[i].amount;
                newOrder[i].amount -= LimitOrder[i].amount;
                LimitOrder[i].amount -= fillAmount;
                newOrder[i].filled = true;
                LimitOrder[i].filled = true;

                uint cost = newOrder[i].amount * newOrder[i].price;
                

                if (newOrder[i].filled && LimitOrder[i].filled) {

                  //transfer funds and update balances
                  balances[msg.sender][ticker] += fillAmount;
                  balances[msg.sender]["ETH"] -= cost;

                  // balances[newOrder[i].trader][ticker] -= fillAmount;
                  // balances[newOrder[i].trader]["ETH"] += cost;

                  newOrder[i] = newOrder[newOrder.length - 1];
                  newOrder.pop();

                  LimitOrder[i] = LimitOrder[LimitOrder.length - 1];
                  LimitOrder.pop();
                }

              }

              //handle case when limit order < market order
              else if (newOrder[i].amount < LimitOrder[i].amount) {
                uint fillAmount = newOrder[i].amount;
                newOrder[i].amount -= fillAmount;
                LimitOrder[i].amount -= fillAmount;
                newOrder[i].filled = true;

                if (newOrder[i].filled) {
                  newOrder[i] = newOrder[newOrder.length - 1];
                  newOrder.pop();

                }

              }

              //handle case when limit order > market order
              else if (newOrder[i].amount > LimitOrder[i].amount) {
                uint fillAmount = LimitOrder[i].amount;
                newOrder[i].amount -= fillAmount;
                LimitOrder[i].amount -= fillAmount;
                LimitOrder[i].filled = true;

                if (LimitOrder[i].filled) {
                  LimitOrder[i] = LimitOrder[newOrder.length - 1];
                  LimitOrder.pop();

                }

              }

          }
    }

#SUMMARY OF THIS POST
Although my code is not fully finished. I feel i achieved a lot today and spent a lot of time figuring out how to solve the market order function. There are links in other pas of my code i have not shown but i will post the full thing tomorrow. But basically i learned how to best go about develiping an algorithm and the pen and paper process really helped me dont think i would have figured this out as quickily otherwise

There will be so many changes i can make to this when i get it completely developed to make it more efficient and perhaps done in less lines of code this is very much a first draft of my approach to the problem

3 Likes

EDIT just pushed to git hub. Feel free to check out my code clone it modify it and look at my other repos and projects on compuational fluid dynamics.**

https://github.com/mcgraneder/SimpleDecentralisedExchange

Hello everyone So i finished my dex today by writing my final tests. I actually spent more of the day refining the my above marketOrder algorithm as i actually had a few big issues and took me most of the day to resolve. However i have finally finished it and i am very satiafied with my result (still plan to do rigourus testing to try break my code to find edge cases i will realise this when i make the front end)… I went off on my own path with this project by adding my own XOR sorting algorithm and my own version of the market order function which was by far the hardest to code. I still plan to spend a lot of time making my whole codebase more efficient as its not by any means as good as it could be. and after i take the react and javascript course im going to code up a really nice front end so stay tuned.

I look forward to follwoing filips solution now and taking tips from his to improve mine because currently my algorithm has many if statments and i should probably set up more variables to help maipulate my code more instead of writing many if statments to reduce gas costs. If anyone has any tips be sure to let me know. I will push my code to github later and edit the post with the repo link when i do. For now here are all of my files.

To summarise i learned so much with this course and it just shows how much further there still is to go with oracles in solidity 201 (old) and then SC security. Im very excited for the next month or two. I also love answering questions on the form ive learned so much and its absolutley a pleasure to contribute and take part in this fabulous community. Thanks guys for being the best and to @dan-i, @thecil thanks for your help along the way and see yous in the next course

Happy coding,
Evan

DEX CONTRACT

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
pragma experimental ABIEncoderV2;

import "./wallet.sol";
import "../node_modules/@openzeppelin//contracts/access/Ownable.sol";

contract Dex is Wallet {

    uint private id = 0;
    int low = 0;
    int highB;
    int highS;
    
    enum Side {
        BUY,
        SELL
    }

    struct Order {

        address trader;
        string ticker;
        uint256 amount;
        Side side;
        uint id;
        uint price;
        bool filled;
    }

    
    mapping(string => mapping(uint => Order[])) LimitOrderBook;
  
  
    function getOrders(Side side, string memory ticker) public view returns(Order[] memory) {
        //return LimitOrderBook[ticker][uint(side)] book
        return (LimitOrderBook[ticker][uint(side)]);
    }

    //to create a limit LimitOrderBook[ticker][uint(side)] we need to pass in the token ticker, whethere it is a buy or sell LimitOrderBook[ticker][uint(side)]
    //the amount of tokens for the LimitOrderBook[ticker][uint(side)] and at what price we want to buy and sell at
    function createLimitOrder(Side side, string memory ticker, uint amount, uint price) public {
        
        //handle first case for LimitOrderBook[ticker][uint(side)] if it is buy(We must have enough eth to make the LimitOrderBook[ticker][uint(side)])
        //the balances mapping is inherited from our wallet contrac
        if (side == Side.BUY) {

          require(balances[msg.sender]["ETH"] >= amount * price);
          require(balances[msg.sender][ticker] >= amount);
          
          
          Order[] storage newOrder = LimitOrderBook[ticker][uint(side)]; 
          highB = int(newOrder.length);
          newOrder.push(Order(msg.sender, ticker, amount, side, id, price, false));
          sortOrder(side, ticker, low, highB);
          settleOrders(ticker, side);
          id++;
          highB++;

        }else {

          require(balances[msg.sender][ticker] >= amount * price);
          require(balances[msg.sender][ticker] >= amount);
          
          
          Order[] storage newOrder = LimitOrderBook[ticker][uint(side)]; 
          highS = int(newOrder.length);
          newOrder.push(Order(msg.sender, ticker, amount, side, id, price, false));
          sortOrder(side, ticker, low, highS);
          settleOrders(ticker, side);
          id++;
          highS++;


        }

    }

    function createMarketOrder(Side side, string memory ticker, uint amount, uint price) public {
     

        if (side == Side.BUY) {

          side = Side.SELL;
        }else {

          side = Side.BUY;
        }
        //handle first case for LimitOrderBook[ticker][uint(side)] if it is buy(We must have enough eth to make the LimitOrderBook[ticker][uint(side)]
        //the balances mapping is inherited from our wallet contrac
        if (side == Side.BUY) {

          require(balances[msg.sender]["ETH"] >= amount * price);
          //require(balances[msg.sender][ticker] >= amount);

          //create new order instance for the buy side
          Order[] storage newOrder = LimitOrderBook[ticker][uint(side)];
          newOrder.push(Order(msg.sender, ticker, amount, side, id, price, false));

          settleOrders(ticker, side);
          

        }else {

          require(balances[msg.sender][ticker] >= amount * price);
          //require(balances[msg.sender][ticker] >= amount);

          Order[] storage newOrder = LimitOrderBook[ticker][uint(side)];
          newOrder.push(Order(msg.sender, ticker, amount, side, id, price, false));

          settleOrders(ticker, side);

         
          
        }

    }

    function settleOrders(string memory ticker, Side side) internal {

      Order[] storage LimitOrder;
      Order[] storage newOrder;
      if (side == Side.BUY) {

        LimitOrder = LimitOrderBook[ticker][uint(Side.SELL)];
        newOrder = LimitOrderBook[ticker][uint(Side.BUY)];

      }else {

        LimitOrder = LimitOrderBook[ticker][uint(Side.BUY)];
        newOrder = LimitOrderBook[ticker][uint(Side.SELL)];
      }
      
      uint i = 0;
      uint cost = 0;
      uint fillAmount;
      while (newOrder.length > 0) {
            
            //handle case when limit order book is empty
            if (LimitOrder.length == 0) {
              break;
            }
            //handle case when market order amount == limit order amt
            if (newOrder[i].amount == LimitOrder[i].amount) {
                fillAmount = 0;
                fillAmount = newOrder[i].amount;
                newOrder[i].amount -= newOrder[i].amount;
                LimitOrder[i].amount -= LimitOrder[i].amount;
                newOrder[i].filled = true;
                LimitOrder[i].filled = true;
                cost = fillAmount * LimitOrder[i].price;
               
               
              }

              //handle case when limit order < market order
              else if (newOrder[i].amount > LimitOrder[i].amount) {
                fillAmount = 0;
                fillAmount = LimitOrder[i].amount;
                newOrder[i].amount -= LimitOrder[i].amount;
                LimitOrder[i].amount -= LimitOrder[i].amount;
                LimitOrder[i].filled = true;
                cost = fillAmount * LimitOrder[i].price;
              }

              //handle case when limit order > market order
              else if (newOrder[i].amount < LimitOrder[i].amount) {
                fillAmount = 0;
                fillAmount = newOrder[i].amount;
                newOrder[i].amount -= newOrder[i].amount;
                LimitOrder[i].amount -= newOrder[i].amount;
                newOrder[i].filled = true;
                cost = fillAmount * LimitOrder[i].price;
               
              }

              // cost = fillAmount * LimitOrder[i].price;
              // //transfer funds and update balances
              if (side == Side.SELL) {

                balances[LimitOrder[i].trader][ticker] += fillAmount;
                balances[LimitOrder[i].trader]["ETH"] -= cost;

                balances[newOrder[i].trader][ticker] -= fillAmount;
                balances[newOrder[i].trader]["ETH"] += cost;

              }else {

                balances[LimitOrder[i].trader][ticker] -= fillAmount;
                balances[LimitOrder[i].trader]["ETH"] += cost;

                balances[newOrder[i].trader][ticker] += fillAmount;
                balances[newOrder[i].trader]["ETH"] -= cost;
              }
              

              if (newOrder[i].filled) {
                  newOrder[i] = newOrder[newOrder.length - 1];
                  newOrder.pop();
              }

              if (LimitOrder[i].filled) {
                LimitOrder[i] = LimitOrder[LimitOrder.length - 1];
                LimitOrder.pop();
                
            }      


          }
          
    }

   
    function sortOrder(Side side, string memory ticker, int low, int high) public {
     // side = Side.BUY;
      if (side == Side.BUY) {

        if (low < high) {
          int pivot = int(LimitOrderBook[ticker][uint(side)][uint(high)].price);
          int i = (low-1); // index of smaller element
          for (int j = low; j < high; j++) {
            if (int(LimitOrderBook[ticker][uint(side)][uint(j)].price) <= pivot) {
              i++;
              if (i != j) {
                LimitOrderBook[ticker][uint(side)][uint(i)].price ^= LimitOrderBook[ticker][uint(side)][uint(j)].price;
                LimitOrderBook[ticker][uint(side)][uint(j)].price ^= LimitOrderBook[ticker][uint(side)][uint(i)].price;
                LimitOrderBook[ticker][uint(side)][uint(i)].price ^= LimitOrderBook[ticker][uint(side)][uint(j)].price;
              }
            }
          }
          if (i+1 != high) {
            LimitOrderBook[ticker][uint(side)][uint(i+1)].price ^= LimitOrderBook[ticker][uint(side)][uint(high)].price;
            LimitOrderBook[ticker][uint(side)][uint(high)].price ^= LimitOrderBook[ticker][uint(side)][uint(i+1)].price;
            LimitOrderBook[ticker][uint(side)][uint(i+1)].price ^= LimitOrderBook[ticker][uint(side)][uint(high)].price;
          }
          pivot = i + 1;
          sortOrder(side, ticker, low, pivot-1);
          sortOrder(side, ticker, pivot+1, high);

        }
      } else {
          if (low < high) {
            int pivot = int(LimitOrderBook[ticker][uint(side)][uint(high)].price);
            int i = (low-1); // index of smaller element
            for (int j = low; j < high; j++) {
              if (int(LimitOrderBook[ticker][uint(side)][uint(j)].price) >= pivot) {
                i++;
                if (i != j) {
                  LimitOrderBook[ticker][uint(side)][uint(i)].price ^= LimitOrderBook[ticker][uint(side)][uint(j)].price;
                  LimitOrderBook[ticker][uint(side)][uint(j)].price ^= LimitOrderBook[ticker][uint(side)][uint(i)].price;
                  LimitOrderBook[ticker][uint(side)][uint(i)].price ^= LimitOrderBook[ticker][uint(side)][uint(j)].price;
                }
              }
            }
            if (i+1 != high) {
              LimitOrderBook[ticker][uint(side)][uint(i+1)].price ^= LimitOrderBook[ticker][uint(side)][uint(high)].price;
              LimitOrderBook[ticker][uint(side)][uint(high)].price ^= LimitOrderBook[ticker][uint(side)][uint(i+1)].price;
              LimitOrderBook[ticker][uint(side)][uint(i+1)].price ^= LimitOrderBook[ticker][uint(side)][uint(high)].price;
            }
            pivot = i + 1;
            sortOrder(side, ticker, low, pivot-1);
            sortOrder(side, ticker, pivot+1, high);

      }
      
      }
    }

}


DEX MARKET ORDER TEST

//next we need to create maekwt order function.
//when creating a sell market order the user should have enought tokens
//when creating a buy order the user should have enough eth
//market orders can be submitted even when the order book is empty
//market orders should be filled until the orderbook is empty or the order is 100% filled
//the eth balance of the buyer should increase with filled orders
//the token balance of the sellers should decrease as orders are filled
//filled limit orders should be removed from the orderbook
const Dex = artifacts.require("Dex");
const Token = artifacts.require("Token");
const Eth = artifacts.require("Eth");


//we need to import mocha to run tests
var truffleAssert = require("truffle-assertions");

//truffle uses mocha to run tests. Each time we define a contract
//statment like below an instance of our deployment is made andinside
//che contratc function we write our tests
contract('Dex', accounts => {
    //initialise test with 'it'

    it("When creating a sell market order the user should have enough tokens", async () => {

        dex = await Dex.deployed()
        eth = await Eth.deployed()
        link = await Token.deployed()

        await dex.addToken("ETH", eth.address)
        await dex.addToken("LINK", link.address)
        await eth.approve(dex.address, 1000)
        await link.approve(dex.address, 1000)

        await dex.deposit(1000, "LINK")
        //await dex.deposit(1000, "LINK")
       
        //test that user has enough eth to settle sell market order
        //cant sell more tokens that you own
        await truffleAssert.passes(await dex.createMarketOrder(0, "LINK", 50 , 2)) 
        await truffleAssert.reverts(dex.createMarketOrder(0, "LINK", 2001 , 1)) 

       
    })

    it("When creating a buy market order the user should have enough eth", async () => {

        dex = await Dex.deployed()
        eth = await Eth.deployed()
        link = await Token.deployed()

        await dex.addToken("ETH", eth.address)
        await dex.addToken("LINK", link.address)
        await eth.approve(dex.address, 1000)
        await link.approve(dex.address, 1000)

        await dex.deposit(1000, "ETH")
        //await dex.deposit(1000, "LINK")
        
        //test that user has enough eth to settle sell market order
        await truffleAssert.passes(await dex.createMarketOrder(1, "LINK", 50 , 2)) 
        await truffleAssert.reverts(dex.createMarketOrder(1, "LINK", 2001 , 1)) 

    })

    it("Market orders can be submitted even if the orderbook is empty", async () => {

        let dex = await Dex.deployed()
        let eth = await Eth.deployed()
        let link = await Token.deployed()

        await dex.addToken("ETH", eth.address)
        await dex.addToken("LINK", link.address)
        await eth.approve(dex.address, 1000)
        await link.approve(dex.address, 1000)

        await dex.deposit(1000, "ETH")
        //await dex.deposit(1000, "LINK")
        
        //test that user has enough eth to settle sell market order
        orderBookBuy = await dex.getOrders(0, "ETH")
        await assert(orderBookBuy.length == 0)
        await truffleAssert.passes(await dex.createMarketOrder(0, "ETH", 50 , 2)) 
        
        orderBookSell = await dex.getOrders(1, "ETH")
        await assert(orderBookSell.length == 1)
        // // await dex.createMarketOrder(0, "LINK", 50 , 2)
        // // await dex.createMarketOrder(0, "LINK", 50 , 2)
        // //dex.createMarketOrder(1, "LINK", 1000001 , 1)) 

        // console.log(orderBookBuy)
        // console.log(orderBookSell)
    

       
    })

    it("Market orders should be filled until 100% filled or else the order book is empty", async () => {

        let dex = await Dex.deployed()
        let eth = await Eth.deployed()
        let link = await Token.deployed()

        await dex.addToken("ETH", eth.address)
        await dex.addToken("LINK", link.address)
        await eth.approve(dex.address, 1000)
        await link.approve(dex.address, 1000)

        await dex.deposit(1000, "ETH")
        await dex.deposit(1000, "LINK")
        
        
        await dex.createMarketOrder(0, "LINK", 50 , 2)
        await dex.createMarketOrder(0, "LINK", 20 , 2)
        await dex.createMarketOrder(0, "LINK", 30 , 2)

        orderBookSell = await dex.getOrders(1, "LINK")

        for (let i = 0; i < orderBookSell.length; i++ ) {

            await assert(orderBookSell[i].filled == false)
        }
        
        orderBookBuy = await dex.getOrders(0, "LINK")
        await assert(orderBookBuy.length == 0)

        await dex.createLimitOrder(0, "LINK", 100 , 2)
        
        orderBookSell = await dex.getOrders(1, "LINK")
        orderBookBuy = await dex.getOrders(0, "LINK")

        await assert(orderBookSell.length == 0)
        await assert(orderBookBuy.length == 0)

        // // await dex.createMarketOrder(0, "LINK", 50 , 2)
        // //dex.createMarketOrder(1, "LINK", 1000001 , 1)) 

        // console.log(orderBookBuy)
        // console.log(orderBookSell)
    
    })

    it("The eth balance of the buyer should decrease with filled orders and likewise the token balance of the seller should decrease", async () => {

        let dex1 = await Dex.deployed()
        let eth1 = await Eth.deployed()
        let link1 = await Token.deployed()

        await dex1.addToken("ETH", eth.address)
        await dex1.addToken("LINK", link.address)

        await link1.transfer(accounts[1], 1000)
        await eth1.transfer(accounts[1], 1000)

        await eth1.approve(dex.address, 1000, {from: accounts[0]})
        await eth1.approve(dex.address, 1000, {from: accounts[1]})
        await link1.approve(dex.address, 1000, {from: accounts[0]})
        await link1.approve(dex.address, 1000, {from: accounts[1]})

        await dex1.deposit(1000, "ETH", {from: accounts[0]})
        await dex1.deposit(1000, "LINK", {from: accounts[0]})
        await dex1.deposit(1000, "ETH", {from: accounts[1]})
        await dex1.deposit(1000, "LINK", {from: accounts[1]})
        //await dex.deposit(1000, "LINK")

        account0EthBalance = await dex1.getBalance(accounts[0], "ETH")
        account0LinkBalance = await dex1.getBalance(accounts[0], "LINK")
        account1EthBalance = await dex1.getBalance(accounts[1], "ETH")
        account1LinkBalance= await dex1.getBalance(accounts[1], "LINK")

        // console.log(account0EthBalance)
        // console.log(account1EthBalance)
        // console.log(account0LinkBalance)
        // console.log(account1LinkBalance)

        assert(account0EthBalance == 4000)
        assert(account0LinkBalance == 3000)
        assert(account1EthBalance == 1000)
        assert(account1LinkBalance == 1000)

        await dex1.createLimitOrder(0, "LINK", 50 , 10, {from: accounts[0]})
        await dex1.createMarketOrder(0, "LINK", 50 , 10, {from: accounts[1]})

        account0EthBalance = await dex1.getBalance(accounts[0], "ETH")
        account0LinkBalance = await dex1.getBalance(accounts[0], "LINK")
        account1EthBalance = await dex1.getBalance(accounts[1], "ETH")
        account1LinkBalance= await dex1.getBalance(accounts[1], "LINK")

        //the eth balance of accounts 0 should reduce
        //the token balance of accounts[1] should increase
        await assert(account0EthBalance == 3500)
        await assert(account0LinkBalance == 3050)
        await assert(account1EthBalance == 1500)
        await assert(account1LinkBalance == 950)
        
       
    })

    
})

It took me longer than I thought to get through this course. I went over everything with a fine toothed comb. It was well worth it and I learned quite a bit about solidity programming and practice. The mechanics for how to put together a project - the dex.
https://github.com/maxgfaraday/babydex
Thanks Filip!!!

2 Likes

nice work man great readMe looks very nice and proffessional

This course was amazing and I can’t thank you enough for this @filip

I will add a front end and probably more functions in the future when I learn more!! I am pumped for the security course next!!

Here is my code!

Wallet:

pragma solidity 0.8.4;

import "../node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../node_modules/@openzeppelin/contracts/utils/math/SafeMath.sol";
import "../node_modules/@openzeppelin/contracts/access/Ownable.sol";

contract Wallet is Ownable {
    using SafeMath for uint256;
    
    struct Token {
        bytes32 ticker;
        address tokenAddress;
    }
    mapping(bytes32 => Token) public tokenMapping;
    bytes32[] public tokenList;


    mapping(address => mapping(bytes32 => uint256)) public balances;

    modifier tokenExist(bytes32 ticker){
        require(tokenMapping[ticker].tokenAddress != address(0), "Token does not exsist!!");
        _;
    }

    function addToken(bytes32 ticker, address tokenAddress) onlyOwner external {
        tokenMapping[ticker] = Token(ticker, tokenAddress);
        tokenList.push(ticker);
    }

    function deposit(uint amount, bytes32 ticker) tokenExist(ticker) external  {
        IERC20(tokenMapping[ticker].tokenAddress).transferFrom(msg.sender, address(this), amount);
        balances[msg.sender][ticker] = balances[msg.sender][ticker].add(amount);


    }

    function withdraw(uint amount, bytes32 ticker) tokenExist(ticker) external {
        require(balances[msg.sender][ticker] >= amount, "Balance not sufficient");
        

        balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(amount);
        IERC20(tokenMapping[ticker].tokenAddress).transfer(msg.sender, amount);
    }
}

Tokens:

pragma solidity 0.8.4;

import "../node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract Link is ERC20 {

    constructor() ERC20("Chainlink", "LINK") public {
        _mint(msg.sender, 1000);
    }


}

Dex:

pragma solidity 0.8.4;
pragma abicoder v2;

import "./wallet.sol";

contract Dex is Wallet {

    using SafeMath for uint256;

    enum Side {
        BUY,
        SELL
    }

    struct Order {
        uint id;
        address trader;
        Side side;
        bytes32 ticker;
        uint amount;
        uint price;
        uint filled;
    }

    uint public nextOrderId;

    mapping(bytes32 => mapping(uint => Order[])) public orderBook;

    function getOrderBook(bytes32 ticker, Side side) view public returns(Order[] memory) {
        return orderBook[ticker][uint(side)];
    }

    function createLimitOrder(Side side, bytes32 ticker, uint amount, uint price) public {
        if(side == Side.BUY) {
            require(balances[msg.sender]["ETH"] >= amount.mul(price), "Balance too low!!");
        }
        if(side == Side.SELL) {
            require(balances[msg.sender][ticker] >= amount, "Balance too low!!");
        }

        Order[] storage orders = orderBook[ticker][uint(side)];
        orders.push(Order(nextOrderId, msg.sender, side, ticker, amount, price, 0));
        Order storage newOrder = orders[orders.length -1];

        uint m = orders.length > 0 ? orders.length - 1 : 0;

        if(side == Side.BUY) {
            while(m >0){
                if(orders[m-1].price > orders[m].price) {
                    break;
                }
                Order memory orderToMove = orders[m - 1];
                orders[m-1] = orders[1];
                orders[m] = orderToMove;
                m--;
            }
        }
        else if (side == Side.SELL){
            while(m > 0) {
                if(orders[m - 1].price < orders[m].price) {
                    break;
                }
                Order memory orderToMove = orders[m-1];
                orders[m-1] = orders[m];
                orders[m] = orderToMove;
                m--;
            }
        }
        nextOrderId++;

    }

    function createMarketOrder(Side side, bytes32 ticker, uint amount) public {
        if(side ==Side.SELL){
            require(balances[msg.sender][ticker] >= amount, "Not enough funds");
        }

        uint orderBookSide;
        if(side == Side.BUY){
            orderBookSide = 1;
        }
        else{
            orderBookSide = 0;
        }
        Order[] storage orders = orderBook[ticker][orderBookSide];

        uint totalFilled = 0;

        for (uint256 m = 0; m < orders.length && totalFilled < amount; m++) {
            uint leftToFill = amount.sub(totalFilled); 
            uint availableToFill = orders[m].amount.sub(orders[m].filled);
            uint filled = 0;
            if(availableToFill > leftToFill){
                filled = leftToFill;
            }
            else{
                filled = availableToFill;
            }

            totalFilled = totalFilled.add(filled);
            orders[m].filled = orders[m].filled.add(filled);
            uint cost = filled.mul(orders[m].price);

            if(side == Side.BUY){
                require(balances[msg.sender]["ETH"] >= cost);

                balances[msg.sender][ticker] = balances[msg.sender][ticker].add(filled);
                balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].sub(cost);

                balances[orders[m].trader][ticker] = balances[orders[m].trader][ticker].sub(filled);
                balances[orders[m].trader]["ETH"] = balances[orders[m].trader]["ETH"].add(cost);
            }
            else if(side == Side.SELL){
                balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(filled);
                balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].add(cost);

                balances[orders[m].trader][ticker] = balances[orders[m].trader][ticker].sub(filled);
                balances[orders[m].trader]["ETH"] = balances[orders[m].trader]["ETH"].sub(cost);
            }

        }
        while(orders.length > 0 && orders[0].filled == orders[0].amount){
            for (uint256 m = 0; m < orders.length - 1; m++) {
                orders[m] = orders[m + 1];
            }
            orders.pop();
        }
    }
}

Tests:

//The user must have ETH deposited such that deposited ETH >= buy order value.
//The user must have enough tokens deposited such that token balance >= sell order amount.
//The BUY order book should be ordered on price from highest to lowest starting at index 0.
//The SELL order should be ordered on price from Lowest to highest starting at index 0.
  
const Dex = artifacts.require("Dex")
const Link = artifacts.require("Link")
const truffleAssert = require('truffle-assertions');

contract.skip("Dex", accounts => {
    //The user must have ETH deposited such that deposited eth >= buy order value
    it("should throw an error if ETH balance is too low when creating BUY limit order", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()
        await truffleAssert.reverts(
            dex.createLimitOrder(0, web3.utils.fromUtf8("LINK"), 10, 1)
        )
        dex.depositEth({value: 10})
        await truffleAssert.passes(
            dex.createLimitOrder(0, web3.utils.fromUtf8("LINK"), 10, 1)
        )
    })
    //The user must have enough tokens deposited such that token balance >= sell order amount
    it("should throw an error if token balance is too low when creating SELL limit order", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()
        await truffleAssert.reverts(
            dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 10, 1)
        )
        await link.approve(dex.address, 500);
        await dex.addToken(web3.utils.fromUtf8("LINK"), link.address, {from: accounts[0]})
        await dex.deposit(10, web3.utils.fromUtf8("LINK"));
        await truffleAssert.passes(
            dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 10, 1)
        )
    })
    //The BUY order book should be ordered on price from highest to lowest starting at index 0
    it("The BUY order book should be ordered on price from highest to lowest starting at index 0", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()
        await link.approve(dex.address, 500);
        await dex.depositEth({value: 3000});
        await dex.createLimitOrder(0, web3.utils.fromUtf8("LINK"), 1, 300); 
        await dex.createLimitOrder(0, web3.utils.fromUtf8("LINK"), 1, 100);
        await dex.createLimitOrder(0, web3.utils.fromUtf8("LINK"), 1, 200);

        let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 0);
        assert(orderbook.length > 0);
        console.log(orderbook);
        for (let i = 0; i < orderbook.length - 1; i++) {
            assert(orderbook[i].price >= orderbook[i+1].price, "not right order in buy book")
        }
    })
    //The SELL order book should be ordered on price from lowest to highest starting at index 0
    it("The SELL order book should be ordered on price from lowest to highest starting at index 0", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()
        await link.approve(dex.address, 500);
        await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 1, 300)
        await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 1, 100)
        await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 1, 200)

        let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1);
        assert(orderbook.length > 0);

        for (let i = 0; i < orderbook.length - 1; i++) {
            assert(orderbook[i].price <= orderbook[i+1].price, "not right order in sell book")
        }
    })

})
const Dex = artifacts.require("Dex")
const Link =artifacts.require("LINK")
const truffleAssert = require('truffle-assertions');

contract("Dex", accounts => {

    //When creating a SELL market ordrer, the seller needs enough tokens for the trade,
    it("Should throw an error if the seller does not have enough tokens for the trade.", async () => {
        let dex = await Dex.deployed()

        let balance = await dex.balances(accounts[0], web3.utils.fromUtf8("LINK"))
        assert.equal(balance.toNumber(), 0, "Inital balance is not 0");

        await truffleAssert.reverts(dex.createMarketOrder(1, web3.utils.fromUtf8("LINK"), 10))
    })

    //Market order can be submited even with a 0 balance on the books.
    it("Should be able to put in market order even when book is empty", async () => {
        let dex = await Dex.deployed()

        await dex.depositEth({value: 50000});

        let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 0); //Getting Buy side orderbook
        assert(orderbook.length == 0, "Buy side Orderbook length is not 0");

        await truffleAssert.passes(dex.createMarketOrder(web3.utils.fromUtf8("LINK"), 10))
    })

    //Market orders will be filled until the order book is empty or the market order is 100% filled
    it("Market orders should not fill more limit orders than the market order amount", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()

        let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1); //Getting Sell side orderbook
        assert(orderbook.length == 0, "Sell side Orderbook should be empty at start of test!");

        await dex.addToken(web3.utils.fromUtf8("LINK"), link.address)

        //Seding Link tokens to accounts 1,2,3 form account 0.
        await link.transfer(accounts[1], 150)
        await link.transfer(accounts[2], 150)
        await link.transfer(accounts[3], 150)

        //Approve DEX for accounts 1,2,3
        await link.approve(dex.address, 50, {from: accounts[1]});
        await link.approve(dex.address, 50, {from: aacounts[2]});
        await link.approve(dex.address, 50, {from: accounts[3]});

        //Deposit LINK into DEX for accounts 1,2,3
        await dex.deposit(50, web3.utils.fromUtf8("LINK"), {from: accounts[1]});
        await dex.deposit(50, web3.utils.fromUtf8("LINK"), {from: accounts[2]});
        await dex.deposit(50, web3.utils.fromUtf8("LINK"), {from: accounts[3]});

        //Fill up the sell order book
        await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 5, 300, {from: accounts[1]})
        await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 5, 400, {from: accounts[2]})
        await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 5, 500, {from: accounts[3]})

        //create market order that should fill 2/3rd's orders in the book
        await dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 10);

        orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1); //Get Sell side orderbook
        assert(orderbook.length == 1, "Sell side Orderbook should onlu have 1 order left");
        assert(orderbook[0].filled == 0, "Sell side order should have 0 filled");
    })

    //Market order should be filled until the order book is empty or the market order is 100% filled
    it("Market orders should be filled until the order book is empty", async () => {
        let dex = await Dex.deployed()
        
        let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1); //Get sell side orderbook
        assert(orderbook.length == 1, "Sell side Orderbook should have 1 order left");

        //Fill up the sell order book again
        await dex.createLimitOrder( 1, web3.utils.fromUtf8("LINK"), 5, 400, {from:accounts[1]})
        await dex.createLimitOrder( 1, web3.utils.fromUtf8("LINK"), 5, 500, {from:accounts[2]})

        //Check buy link balance before link purchase
        let balanceBefore = await dex.balances(accounts[0], web3.utils.fromUtf8("LINK"))

        //Create market order that could fill more that the entire order book (15 link)
        await dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 50);
        
        //Check buy link balance after purchase
        let balanceAfter = await dex.balances(accounts[0], web3.utils.fromUtf8("LINK"))

        //Buyer should have 15 more link after, even though order was for 50.
        assert.equal(balancesBefore.toNumber() + 15, balanceAfter.toNumber());
    })

    //The Eth balance of the buyer should decrease with the filled amount
    it("The Eth balance of the buyer shoud decrease with the filled amount", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()

        //Seller deposits link and creates a sell limt order for 1 link for 300 wei
        await link.approve(dex.address, 500, {from: accounts[1]});
        await createLimitOrder(1,  web3.utils.fromUtf8("LINK"), 1, 300, {from: accounts[1]})

        //Check buyer ETH balance before trade
        let balanceBefore = await dex.balances(accounts[0], web3.utils.fromUtf8("ETH"));
        await dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 1);
        let balanceAfter = await dex.balances(accounts[0], web3.utils.fromUtf8("ETH"));

        assert.equal(balanceBefore.toNumber() - 300, balanceAfter.toNumber());
    })

    //The token balances of the limit order sellers hsould decrease with the filled amounts.
    it("The token balances of the limit order sellers hsould decrease with the filled amounts.", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()

        let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1); //Getting the sell side orderbook
        assert(orderbook.length == 0, "Sell side Orderbook should be empty at start of test");

        //Seller account[2] deposits link
        await link.approve(dex.address, 500, {from: accounts[2]});
        await dex.deposit(100, web3.utils.fromUtf8("LINK"), {from: accounts[2]});

        await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 1, 300, {from: accounts[1]})
        await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 1, 400, {from: accounts[2]})

        //Check balances of sellers before trade
        let account1BalanceBefore = await dex.balances(accounts[1], web3.utils.fromUtf8("LINK"));
        let account2BalanceBefore = await dex.balances(accounts[2], web3.utils.fromUtf8("LINK"));

        //Acount[0] created markety order to buy up both sell orders
        await dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 2);

        //Check sellers link balances after trade
        let account1BalanceAfter = await dex.balances(accounts[1], web3.utils.fromUtf8("LINK"));
        let account2BalanceAfter = await dex.balances(accounts[2], web3.utils.fromUtf8("LINK"));

        assert.equal(account1BalanceBefore.toNumber() - 1, account1BalanceAfter.toNumber());
        assert.equal(account2BalanceBefore.toNumber() - 1, account2BalanceAfter.toNumber());
    })

    //Filled limit orders should be removed from the orderbook
    it("Filled llimit orders should be removed from the orderbook", async () => {
        let dex = await dex.deployed()
        let link = await Link.deployed()
        await dex.addToken(web3.utils.fromUtf8("LINK"), link.address)

        //Seller deposits link and creates a sell limit order for 1 link
        await link.approve(dex.address, 500);
        await dex.deposit(50, web3.utils.fromUtf8("LINK"));

        await dex.depositEth({value: 10000});

        let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1); //Gettting sell side orderbook

        await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 1, 300)
        await dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 1);

        orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1); //Getting sell side orderbook
        assert(orderbook.length == 0, "Sell side Orderbook should be empty after trade");
    })

    //Partly filled lim,it orders should be modified to represent the filled/remaining amount
    it("Limit orders filled properley should be set correcctly after a trade", async () => {
        let dex = await Dex.deployed()

        let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1); //Getting sell side orderbook
        assert(orderbook.length == 0, "Sell side orderbook should be empty at start of test");

        await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 5, 300, {from:accounts[1]})
        await dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 2);

        orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1); //Getting the sell side orderbook
        assert.equal(orderbook[0].filled, 2);
        assert.equal(orderbook[0].amount,  5);
    })

      //When creating a BUY market order, the buyer needs to have enough ETH for the trade
      it("Should throw an error when creating a buy market order without adequate ETH balance", async () => {
        let dex = await Dex.deployed()
        
        let balance = await dex.balances(accounts[4], web3.utils.fromUtf8("ETH"))
        assert.equal( balance.toNumber(), 0, "Initial ETH balance is not 0" );
        await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 5, 300, {from: accounts[1]})

        await truffleAssert.reverts(
            dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 5, {from: accounts[4]})
        )
    })

})
const Dex = artifacts.require("Dex")
const Link = artifacts.require("Link")
const truffleAssert = require('truffle-assertions');

contract.skip("Dex", accounts => {
    it("should only be possible for owner to add tokens", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()
        await truffleAssert.passes(
            dex.addToken(web3.utils.fromUtf8("LINK"), link.address, {from: accounts[0]})
        )
        await truffleAssert.reverts(
            dex.addToken(web3.utils.fromUtf8("AAVE"), link.address, {from: accounts[1]})
        )
    })
    it("should handle deposits correctly", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()
        await link.approve(dex.address, 500);
        await dex.deposit(100, web3.utils.fromUtf8("LINK"));
        let balance = await dex.balances(accounts[0], web3.utils.fromUtf8("LINK"))
        assert.equal( balance.toNumber(), 100 )
    })
    it("should handle faulty withdrawals correctly", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()
        await truffleAssert.reverts(dex.withdraw(500, web3.utils.fromUtf8("LINK")))
    })
    it("should handle correct withdrawals correctly", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()
        await truffleAssert.passes(dex.withdraw(100, web3.utils.fromUtf8("LINK")))
    })
    it("should deposit the correct amount of ETH", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()
        await dex.depositEth({value: 1000});
        let balance = await dex.balances(accounts[0], web3.utils.fromUtf8("ETH"))
        assert.equal(balance.toNumber(), 1000);
    })
    it("should withdraw the correct amount of ETH", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()
        await dex.withdrawEth(1000);
        let balance = await dex.balances(accounts[0], web3.utils.fromUtf8("ETH"))
        assert.equal(balance.toNumber(), 0);
    })
    it("should not allow over-withdrawing of ETH", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()
        await truffleAssert.reverts(dex.withdrawEth(100));
    })
})
2 Likes

Wow, I see you put so much effort in all the homeworks, well done mate!
The comments on each line of code are a huge help for noobs haha - thanks for that!

I was searching for depositETH() implementation to verify something that I was not sure enough and your answers come up in the search results first, just so you know :slight_smile:

So, I I thought I might as well share my thoughts that made me search the forum in the first place, in case you or someone else knows the answer - maybe can be helpful to other students as well.

I was wondering about the
balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].add(msg.value);
part - maybe there are actions that the contract performs that might use up some of the deposited ETH as gas.

But the ETH balance never takes that into account.
It only updates the balance and never touches it unless a BUY or SELL is made.
However, say creating an order with createLimitOrder should also utilize some gas, would not it?

Is the above thinking even correct or does the deposited ETH really stay intact and is not used up by any operations other than BUY / SELL?

here is my dex:
dex.sol doesn’t have a createMarketOrder, it uses the function createOrder for both, setting the price to zero to indicate a market order

// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
pragma experimental ABIEncoderV2;

import "./wallet.sol";

contract Dex is Wallet {

    enum Side {
        BUY,
        SELL
    }

    struct Order {
        uint id;
        address trader;
        Side side;
        bytes32 ticker;
        uint amount;
        uint price;
    }
    uint public nextOrderId;
    uint internal lastPrice;
    bytes32 ethTicker;

    mapping(bytes32 => mapping(Side => Order[])) public orderBook;

    event orderCreated(uint orderId, Side side, bytes32 ticker, uint amount, uint price);
    event marketOrderBothSidesUsingPreviousTradePrice(uint lastPrice);
    event tradeExecuted(uint sellOrderId, uint sellRemaining, uint buyOrderId, uint buyRemaining, bytes32 ticker, uint amount, uint price);

    constructor() {
        ethTicker = bytes32("ETH");
    }

    function getOrderBook(bytes32 ticker, Side side) public view returns (Order[] memory) {
        return orderBook[ticker][side];
    }

    function swapWithPrevious(Order[] storage orders, uint index) private {
        Order memory swap1 = orders[index];
        Order memory swap2 = orders[index-1];
        orders[index] = swap2;
        orders[index-1] = swap1;
    }

    function bubblesort(Order[] storage orders, Side side) private {
        if (orders.length > 1) {
            for (uint index = orders.length-1; index > 0; index--) {
                if ((side == Side.BUY && orders[index-1].price > orders[index].price) ||
                    (side == Side.SELL && orders[index-1].price < orders[index].price)) {
                    swapWithPrevious(orders, index);
                }
                else {
                    break;  // if swap is not needed, we're done
                }
            }
        }
    }

    /* @dev creates an order.  If the price is zero, it a market order
     */
    function createOrder(Side side, bytes32 ticker, uint amount, uint price) external {
        require(ticker != ethTicker, "Buy/Sell orders for ETH are not allowed");
        require(side == Side.BUY || side == Side.SELL, "Buy/Sell are the only two values allowed");
        Order[] storage sellOrders = orderBook[ticker][Side.SELL];
        Order[] storage buyOrders = orderBook[ticker][Side.BUY];
        if (side == Side.BUY) {
            if (price == 0) {
                // if buying at market price, the orders need to be checked to determine the price
                uint amountToPrice = amount;
                uint cost = 0;
                for (uint sellIndex = sellOrders.length; amountToPrice > 0 && sellIndex != 0; sellIndex--) {
                    if (amount < sellOrders[sellIndex-1].amount) {
                        // partial sell order
                        cost += amountToPrice * sellOrders[sellIndex-1].price;
                        amountToPrice = 0;
                    }
                    else {
                        // full sell order
                        cost += sellOrders[sellIndex-1].amount * sellOrders[sellIndex-1].price;
                        amountToPrice -= sellOrders[sellIndex-1].amount;
                    }
                }
                // if amountToPrice is not zero, the sell orderbook will be emptied with more buys needed
                // now check if they have enough ETH to cover the cost
                require(balances[msg.sender][ethTicker] >= cost, "Not enough ETH to create buy order");
            }
            else {
                require(balances[msg.sender][ethTicker] >= amount * price, "Not enough ETH to create buy order");
            }
        }
        else if (side == Side.SELL) {
            require(balances[msg.sender][ticker] >= amount, "Not enough tokens to create sell order");
        }
        Order[] storage orders = orderBook[ticker][side];
        orders.push(Order(nextOrderId, msg.sender, side, ticker, amount, price));
        emit orderCreated(nextOrderId, side, ticker, amount, price);
        nextOrderId++;
        bubblesort(orders, side);

        // // now execute matching orders bottom-of-sell, top-of-buy
        while (
            sellOrders.length > 0 &&
            buyOrders.length > 0 &&
            (sellOrders[sellOrders.length-1].price <= buyOrders[buyOrders.length-1].price ||
            sellOrders[sellOrders.length-1].price == 0 ||
            buyOrders[buyOrders.length-1].price == 0))
        {
            uint currentPrice;
            if (sellOrders[sellOrders.length-1].price == 0 && buyOrders[buyOrders.length-1].price == 0) {
                // the two books have market orders, use the last price encountered to fulfill
                require(lastPrice != 0, "the two books have market orders to execute but no price was set by a previous trade");
                currentPrice = lastPrice;
                emit marketOrderBothSidesUsingPreviousTradePrice(lastPrice);
                require(currentPrice != 0, "zero price calculated (137)");
            }
            else {
                currentPrice = sellOrders[sellOrders.length-1].price;
                if (currentPrice == 0) {
                    // got a market sell order, use the buy price
                    currentPrice = buyOrders[buyOrders.length-1].price;
                }
                require(currentPrice != 0, "zero price calculated (146)");
            }
            Order storage sellOrder = sellOrders[sellOrders.length-1];
            Order storage buyOrder = buyOrders[buyOrders.length-1];
            uint amountToTransact = sellOrder.amount < buyOrder.amount ? sellOrder.amount : buyOrder.amount;
            // make sure the buyer has enough ETH.  If the next check fails, how do you remove the bad buy?
            require(balances[buyOrder.trader][ethTicker] >= amountToTransact * currentPrice, "buyer doesn't have enough ETH to cover the executing trade");
            require(currentPrice != 0, "zero price calculated");

            // send tokens from the seller to the buyer, ETH from buyer to seller
            sellOrder.amount -= amountToTransact;
            buyOrder.amount -= amountToTransact;
            balances[sellOrder.trader][sellOrder.ticker] -= amountToTransact;
            balances[buyOrder.trader][ethTicker] -= amountToTransact * currentPrice;
            balances[sellOrder.trader][ethTicker] += amountToTransact * currentPrice;
            balances[buyOrder.trader][sellOrder.ticker] += amountToTransact;

            emit tradeExecuted(sellOrder.id, sellOrder.amount, buyOrder.id, buyOrder.amount, sellOrder.ticker, amountToTransact, currentPrice);
            // at least one of the orders has been fulfilled, it should be removed
            if (sellOrder.amount == 0) {
                sellOrders.pop();
            }
            if (buyOrder.amount == 0) {
                buyOrders.pop();
            }
        }
    }
}

tokens.sol: LINK token

// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;

import "../node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract Link is ERC20 {
    constructor() ERC20("Chainlink", "LINK") {
        _mint(msg.sender, 1000000000);
    }
}

wallet.sol: wallet code

// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;

import "../node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../node_modules/@openzeppelin/contracts/access/Ownable.sol";

contract Wallet is Ownable {

    struct Token {
        bytes32 ticker;
        address tokenAddress;
    }
    bytes32[] public tokenList;
    mapping(bytes32 => Token) public tokenMapping;

    mapping (address => mapping(bytes32 => uint256)) public balances;

    event tokenAdded(bytes32 ticker);
    event depositted(address sender, uint amount, bytes32 ticker);
    event withdrawn(address sender, uint amount, bytes32 ticker);
    event ethDepositted(address sender, uint amount, bytes32 ticker);
    event ethWithdrawn(address sender, uint amount, bytes32 ticker);

    function addToken(bytes32 ticker, address tokenAddress) onlyOwner external {
        if (tokenMapping[ticker].tokenAddress == address(0)) {
            tokenMapping[ticker] = Token(ticker, tokenAddress);
            tokenList.push(ticker);
            emit tokenAdded(ticker);
        }
    }

    function deposit(uint amount, bytes32 ticker) external {
        require(tokenMapping[ticker].tokenAddress != address(0), "Token ticker has not been added, call addToken with the details");
        balances[msg.sender][ticker] += amount;
        IERC20(tokenMapping[ticker].tokenAddress).transferFrom(msg.sender, address(this), amount);
        emit depositted(msg.sender, amount, ticker);
    }

    function withdraw(uint amount, bytes32 ticker) external {
        require(tokenMapping[ticker].tokenAddress != address(0), "Token ticker has not been added, call addToken with the details");
        require(balances[msg.sender][ticker] >= amount, "Insufficient funds");
        balances[msg.sender][ticker] -= amount;
        IERC20(tokenMapping[ticker].tokenAddress).transfer(msg.sender, amount);
        emit withdrawn(msg.sender, amount, ticker);
    }

    function depositEth() public payable {
        balances[msg.sender]["ETH"] += msg.value;
        emit ethDepositted(msg.sender, msg.value, "ETH");
    }

    function withdrawEth(uint amount) public {
        balances[msg.sender]["ETH"] -= amount;
        emit ethWithdrawn(msg.sender, amount, "ETH");
    }
}

wallettest.js:


const DexMock = artifacts.require("DexMock");
const Link = artifacts.require("Link");
const truffleAssert = require("truffle-assertions")

contract("Wallet", accounts => {
    it("should only be possible for owners to add tokens", async () => {
        let dex = await DexMock.deployed()
        let link = await Link.deployed()
        let symbol = web3.utils.fromUtf8("LINK")
        let [alice, bob, chuck, david] = accounts
        await truffleAssert.passes(
            dex.addToken(symbol, link.address, {from: alice})
        )
        await truffleAssert.reverts(
            dex.addToken(symbol, link.address, {from: bob})
        )
    })
    it("should handle deposits/withdrawls correctly", async () => {
        let dex = await DexMock.deployed()
        let link = await Link.deployed()
        let symbol = web3.utils.fromUtf8("LINK")
        let [alice, bob, chuck, david] = accounts
        await link.approve(dex.address, 500)
        await dex.deposit(100, symbol)
        let balanceOfLink = await dex.balances(alice, symbol)
        assert.equal(balanceOfLink, 100)
        await truffleAssert.reverts(
            dex.withdraw(200, symbol, {from: alice})
        )
        balanceOfLink = await dex.balances(alice, symbol)
        assert.equal(balanceOfLink, 100)
        await truffleAssert.passes(
            dex.withdraw(100, symbol)
        )
        balanceOfLink = await dex.balances(alice, symbol)
        assert.equal(balanceOfLink, 0)
    })
})

dextest.js

const DexMock = artifacts.require("DexMock");
const Link = artifacts.require("Link");
const truffleAssert = require("truffle-assertions")
const BUY = 0
const SELL = 1
let linkTicker = web3.utils.fromUtf8("LINK")
let ethTicker = web3.utils.fromUtf8("ETH")

async function withdrawAll(dex, account, ticker) {
    let totalSupply = await dex.balances(account, ticker)
    //console.log("withdrawAll: totalSupply=" + totalSupply.toNumber())
    if (totalSupply > 0) {
        if (ticker === ethTicker) {
            await dex.withdrawEth(totalSupply, {from: account})
        }
        else {
            await dex.withdraw(totalSupply, ticker, {from: account})
        }
    }
    let newSupply = await dex.balances(account, ticker)
    //console.log("withdrawAll: newSupply=" + newSupply.toNumber())
    assert(newSupply == 0, "withdrawAll didn't remove all of the supply")
    return totalSupply
}

contract("DexMock", accounts => {
    it("user must have enough eth to cover the buy order", async () => {
        let dex = await DexMock.deployed()
        let [alice, bob, chuck, david] = accounts
        await dex.clearOrderBook(BUY, linkTicker)
        await dex.clearOrderBook(SELL, linkTicker)
        await truffleAssert.reverts(
            dex.createOrder(BUY, linkTicker, 10, 1)
        )
        await dex.depositEth({value: 10})
        await truffleAssert.passes(
            dex.createOrder(BUY, linkTicker, 10, 1)
        )
    })
    it("user should have enough tokens for the sell order", async () => {
        let dex = await DexMock.deployed()
        let link = await Link.deployed()
        let [alice, bob, chuck, david] = accounts
        await dex.clearOrderBook(BUY, linkTicker)
        await dex.clearOrderBook(SELL, linkTicker)
        await truffleAssert.reverts(
            dex.createOrder(SELL, linkTicker, 10, 1)
        )
        await link.approve(dex.address, 500)
        await dex.addToken(linkTicker, link.address, {from: alice})
        await dex.deposit(10, linkTicker)
        await truffleAssert.passes(
            dex.createOrder(SELL, linkTicker, 10, 1)
        )
    })
    it("buy order book should always be ascending in price", async () => {
        let dex = await DexMock.deployed()
        let link = await Link.deployed()
        let [alice, bob, chuck, david] = accounts
        await dex.clearOrderBook(BUY, linkTicker)
        await dex.clearOrderBook(SELL, linkTicker)
        await dex.depositEth({value: 2000})
        await link.approve(dex.address, 500)
        let orderBook = await dex.getOrderBook(linkTicker, BUY);
        let orderCount = orderBook.length;
        await dex.createOrder(BUY, linkTicker, 3, 300)
        await dex.createOrder(BUY, linkTicker, 2, 100)
        await dex.createOrder(BUY, linkTicker, 1, 200)
        orderBook = await dex.getOrderBook(linkTicker, BUY);
        //console.log(orderBook)
        assert(orderCount+3 == orderBook.length, "number of orders didn't go up by 3")
        for (let index = 0; index < orderBook.length - 1; index++) {
            assert(orderBook[index].price <= orderBook[index+1].price, "Prices not in order in buy orderBook")
        }
    })
    it("sell order book should always be descending in price", async () => {
        let dex = await DexMock.deployed()
        let link = await Link.deployed()
        let [alice, bob, chuck, david] = accounts
        await dex.clearOrderBook(BUY, linkTicker)
        await dex.clearOrderBook(SELL, linkTicker)
        await dex.depositEth({value: 2000})
        await link.approve(dex.address, 500)
        let orderBook = await dex.getOrderBook(linkTicker, SELL);
        let orderCount = orderBook.length;
        await dex.createOrder(SELL, linkTicker, 3, 300)
        await dex.createOrder(SELL, linkTicker, 2, 100)
        await dex.createOrder(SELL, linkTicker, 1, 200)
        orderBook = await dex.getOrderBook(linkTicker, SELL);
        //console.log(orderBook)
        assert(orderCount+3 == orderBook.length, "number of orders didn't go up by 3")
        for (let index = 0; index < orderBook.length - 1; index++) {
            assert(orderBook[index].price >= orderBook[index+1].price, "Prices not in order in sell orderBook")
        }
    })
})

dextest-market.js

const DexMock = artifacts.require("DexMock");
const Link = artifacts.require("Link");
const { reverts } = require("truffle-assertions");
const truffleAssert = require("truffle-assertions")
const BUY = 0
const SELL = 1
let linkTicker = web3.utils.fromUtf8("LINK")
let ethTicker = web3.utils.fromUtf8("ETH")

async function withdrawAll(dex, account, ticker) {
    let totalSupply = await dex.balances(account, ticker)
    if (totalSupply > 0) {
        if (ticker === ethTicker) {
            await dex.withdrawEth(totalSupply, {from: account})
        }
        else {
            await dex.withdraw(totalSupply, ticker, {from: account})
        }
    }
    let newSupply = await dex.balances(account, ticker)
    assert(newSupply == 0, "withdrawAll didn't remove all of the supply")
    return totalSupply
}

async function createDeposit(owner, account, ticker, amount) {
    let dex = await DexMock.deployed()
    let link = await Link.deployed()
    if (ticker == ethTicker) {
        await dex.depositEth({value: amount, from: account})
    }
    else {
        await link.approve(dex.address, amount, {from: owner})
        await dex.deposit(amount, linkTicker, {from: owner})
        await link.transfer(account, amount, {from: owner})
        await link.approve(dex.address, amount, {from: account})
        await dex.deposit(amount, linkTicker, {from: account})
    }
}

async function getBalances(dex, accounts, ticker) {
    let balances = []
    for (let index = 0; index < accounts.length; index++) {
        balances[index] = await dex.balances(accounts[index], ticker)
    }
    return balances;
}

function getValuesString(balances) {
    let msg = "[";
    for (let index = 0; index < balances.length; index++) {
        if (index != 0) {
            msg += ", "
        }
        msg += balances[index]
    }
    msg += "]"
    return msg
}

async function validateDiffs(msg, prevousBalances, afterBalances, expectedDiffs) {
    for (let index = 0; index < expectedDiffs.length; index++) {
        if (prevousBalances[index].toNumber()+expectedDiffs[index] != afterBalances[index].toNumber()) {
            console.log(msg + " index: " + index + " " + prevousBalances[index].toNumber() + "+" + expectedDiffs[index] + "==" + afterBalances[index].toNumber())
            console.log("Prev: " + getValuesString(prevousBalances))
            console.log("Post: " + getValuesString(afterBalances))
            assert.equal(prevousBalances[index].toNumber()+expectedDiffs[index], afterBalances[index].toNumber())
        }
    }
}
async function clearBalances(dex, accounts, tickers) {
    for (let tickerIndex = 0; tickerIndex < tickers.length; tickerIndex++) {
        for (let accountIndex = 0; accountIndex < accounts.length; accountIndex++) {
            await withdrawAll(dex, accounts[accountIndex], tickers[tickerIndex])
        }
    }
}

contract("DexMock-market", accounts => {
    it("for SELL market order, seller needs enough tokens to sell", async () => {
        let dex = await DexMock.deployed()
        let link = await Link.deployed()
        let [alice, bob, chuck, david] = accounts
        await dex.clearOrderBook(BUY, linkTicker)
        await dex.clearOrderBook(SELL, linkTicker)
        await dex.addToken(linkTicker, link.address, {from: alice})

        // zero out the link supply to make sure the supply is as expected
        await withdrawAll(dex, alice, linkTicker)
        await link.approve(dex.address, 10)
        await dex.deposit(10, linkTicker)
        await truffleAssert.reverts(
             dex.createOrder(SELL, linkTicker, 20, 0)
        )
        await truffleAssert.passes(
            dex.createOrder(SELL, linkTicker, 10, 0)
       )
    })
    it("for BUY market order the buyer needs enough ETH for the trade", async () => {
        let dex = await DexMock.deployed()
        let link = await Link.deployed()
        let [alice, bob, chuck, david] = accounts
        await dex.clearOrderBook(BUY, linkTicker)
        await dex.clearOrderBook(SELL, linkTicker)

        await withdrawAll(dex, bob, ethTicker)
        await withdrawAll(dex, chuck, ethTicker)
        await withdrawAll(dex, david, ethTicker)

        await truffleAssert.reverts(
            // should fail since there isn't any ETH
            dex.createOrder(BUY, linkTicker, 100, 300, {from: bob})
        )
        await truffleAssert.reverts(
            // should fail since there isn't any ETH
            dex.createOrder(BUY, linkTicker, 100, 300, {from: chuck})
        )
        await truffleAssert.reverts(
            // should fail since there isn't any ETH
            dex.createOrder(BUY, linkTicker, 100, 300, {from: david})
        )

        let bobEth = 100*300
        let chuckEth = 200*100
        let davidEth = 300*200

        await dex.depositEth({value: bobEth, from: bob})
        await dex.depositEth({value: chuckEth, from: chuck})
        await dex.depositEth({value: davidEth, from: david})
        
        await dex.createOrder(BUY, linkTicker, 100, 300, {from: bob})
        await dex.createOrder(BUY, linkTicker, 200, 100, {from: chuck})
        await dex.createOrder(BUY, linkTicker, 300, 200, {from: david})
    })
    it("market SELL orders can be submitted even if the BUY order book is empty", async () => {
        let dex = await DexMock.deployed()
        let link = await Link.deployed()
        let [alice, bob, chuck, david] = accounts
        await dex.clearOrderBook(SELL, linkTicker)
        await dex.clearOrderBook(BUY, linkTicker)
        await dex.createOrder(SELL, linkTicker, 10, 0)
        let orderBook = await dex.getOrderBook(linkTicker, SELL);
        //console.log(orderBook)
        assert(orderBook.length == 1, "createOrder failed to add to the order book")
    })
    it("market BUY orders can be submitted even if the SELL order book is empty", async () => {
        let dex = await DexMock.deployed()
        let link = await Link.deployed()
        let [alice, bob, chuck, david] = accounts
        await dex.clearOrderBook(SELL, linkTicker)
        await dex.clearOrderBook(BUY, linkTicker)
        await dex.createOrder(BUY, linkTicker, 10, 0)
        let orderBook = await dex.getOrderBook(linkTicker, BUY);
        //console.log(orderBook)
        assert(orderBook.length == 1, "createMarketOrder failed to add to the order book")
    })
    it("market BUY orders should be filled until the SELL order book is empty", async () => {
        let dex = await DexMock.deployed()
        let link = await Link.deployed()
        let [alice, bob, chuck, david, edward] = accounts
        await dex.clearOrderBook(SELL, linkTicker)
        await dex.clearOrderBook(BUY, linkTicker)

        //console.log("creating deposits")
        await createDeposit(alice, bob, linkTicker, 200)
        await createDeposit(alice, chuck, linkTicker, 100)
        await createDeposit(alice, david, linkTicker, 300)
        await createDeposit(alice, edward, ethTicker, 200*300 + 100*200 + 300*100)

        //console.log("getting balances")
        let linkPrevousBalances = await getBalances(dex, accounts, linkTicker)
        let ethPrevousBalances = await getBalances(dex, accounts, ethTicker)
        
        //console.log("creating sell orders")
        await dex.createOrder(SELL, linkTicker, 200, 300, {from: bob})
        await dex.createOrder(SELL, linkTicker, 100, 200, {from: chuck})
        await dex.createOrder(SELL, linkTicker, 300, 100, {from: david})

        //console.log("checking sell orders")
        let sellOrderBook = await dex.getOrderBook(linkTicker, SELL);
        assert(sellOrderBook.length == 3, "SELL order book should have 3 orders")

        //console.log("creating buy order")
        await dex.createOrder(BUY, linkTicker, 600, 0, {from: edward})
        sellOrderBook = await dex.getOrderBook(linkTicker, SELL);
        let buyOrderBook = await dex.getOrderBook(linkTicker, BUY);
        assert(sellOrderBook.length == 0, "SELL order book should be empty")
        assert(buyOrderBook.length == 0, "BUY order book should be empty")

        //console.log("getting balances again")
        let linkAfterBalances = await getBalances(dex, accounts, linkTicker)
        let ethAfterBalances = await getBalances(dex, accounts, ethTicker)
        let expectedLinkDiffs = [0, -200, -100, -300, 600]
        let expectedEthDiffs = [0, 60000, 20000, 30000, -110000]
        await validateDiffs("LINK", linkPrevousBalances, linkAfterBalances, expectedLinkDiffs)
        await validateDiffs("ETH", ethPrevousBalances, ethAfterBalances, expectedEthDiffs)
    })
    it("market SELL orders should be filled until the BUY order book is empty", async () => {
        let dex = await DexMock.deployed()
        let link = await Link.deployed()
        let [alice, bob, chuck, david, edward] = accounts
        await dex.clearOrderBook(SELL, linkTicker)
        await dex.clearOrderBook(BUY, linkTicker)
        await clearBalances(dex, accounts, [linkTicker, ethTicker])

        await dex.depositEth({value: 200*300, from: bob})
        await dex.depositEth({value: 100*200, from: chuck})
        await dex.depositEth({value: 300*100, from: david})
        await createDeposit(alice, edward, linkTicker, 600)
        
        let linkPrevousBalances = await getBalances(dex, accounts, linkTicker)
        let ethPrevousBalances = await getBalances(dex, accounts, ethTicker)

        await dex.createOrder(BUY, linkTicker, 200, 300, {from: bob})
        await dex.createOrder(BUY, linkTicker, 100, 200, {from: chuck})
        await dex.createOrder(BUY, linkTicker, 300, 100, {from: david})

        await dex.createOrder(SELL, linkTicker, 600, 0, {from: edward})
        let sellOrderBook = await dex.getOrderBook(linkTicker, SELL);
        let buyOrderBook = await dex.getOrderBook(linkTicker, BUY);
        assert(sellOrderBook.length == 0, "SELL order book should be empty")
        assert(buyOrderBook.length == 0, "BUY order book should be empty")

        // balances should increase after SELL
        let linkAfterBalances = await getBalances(dex, accounts, linkTicker)
        let ethAfterBalances = await getBalances(dex, accounts, ethTicker)
        let expectedLinkDiffs = [0, 200, 100, 300, -600]
        let expectedEthDiffs = [0, -60000, -20000, -30000, 110000]
        await validateDiffs("LINK", linkPrevousBalances, linkAfterBalances, expectedLinkDiffs)
        await validateDiffs("ETH", ethPrevousBalances, ethAfterBalances, expectedEthDiffs)
    })
    it("market BUY orders should be filled until the SELL order book is fulfilled", async () => {
        let dex = await DexMock.deployed()
        let link = await Link.deployed()
        let [alice, bob, chuck, david, edward] = accounts
        await clearBalances(dex, accounts, [linkTicker, ethTicker])
        await dex.clearOrderBook(SELL, linkTicker)
        await dex.clearOrderBook(BUY, linkTicker)

        await createDeposit(alice, bob, linkTicker, 200)
        await createDeposit(alice, chuck, linkTicker, 100)
        await createDeposit(alice, david, linkTicker, 300)
        await createDeposit(alice, edward, ethTicker, 200*300 + 100*200 + 300*150)

        let linkPrevousBalances = await getBalances(dex, accounts, linkTicker)
        let ethPrevousBalances = await getBalances(dex, accounts, ethTicker)

        await dex.createOrder(SELL, linkTicker, 200, 300, {from: bob})
        await dex.createOrder(SELL, linkTicker, 100, 200, {from: chuck})
        await dex.createOrder(SELL, linkTicker, 300, 100, {from: david})

        await dex.createOrder(BUY, linkTicker, 650, 0, {from: edward})
        let sellOrderBook = await dex.getOrderBook(linkTicker, SELL);
        let buyOrderBook = await dex.getOrderBook(linkTicker, BUY);
        assert(sellOrderBook.length == 0, "SELL order book should be empty")
        assert(buyOrderBook.length == 1, "BUY order book should have 1 order")
        assert(buyOrderBook[buyOrderBook.length-1].amount == 50, "50 should be remaining to be filled")

        let linkAfterBalances = await getBalances(dex, accounts, linkTicker)
        let ethAfterBalances = await getBalances(dex, accounts, ethTicker)
        let expectedLinkDiffs = [0, -200, -100, -300, 600]
        let expectedEthDiffs = [0, 60000, 20000, 30000, -110000]
        await validateDiffs("LINK", linkPrevousBalances, linkAfterBalances, expectedLinkDiffs)
        await validateDiffs("ETH", ethPrevousBalances, ethAfterBalances, expectedEthDiffs)
    })
    it("market SELL orders should be filled until the BUY order book is fulfilled", async () => {
        let dex = await DexMock.deployed()
        let link = await Link.deployed()
        let [alice, bob, chuck, david, edward] = accounts
        await dex.clearOrderBook(SELL, linkTicker)
        await dex.clearOrderBook(BUY, linkTicker)
        await clearBalances(dex, accounts, [linkTicker, ethTicker])

        await createDeposit(alice, bob, ethTicker, 200*300)
        await createDeposit(alice, chuck, ethTicker, 100*200)
        await createDeposit(alice, david, ethTicker, 300*100)
        await createDeposit(alice, edward, linkTicker, 650)

        let linkPrevousBalances = await getBalances(dex, accounts, linkTicker)
        let ethPrevousBalances = await getBalances(dex, accounts, ethTicker)

        await dex.createOrder(BUY, linkTicker, 200, 300, {from: bob})
        await dex.createOrder(BUY, linkTicker, 100, 200, {from: chuck})
        await dex.createOrder(BUY, linkTicker, 300, 100, {from: david})

        await dex.createOrder(SELL, linkTicker, 650, 0, {from: edward})
        let sellOrderBook = await dex.getOrderBook(linkTicker, SELL);
        let buyOrderBook = await dex.getOrderBook(linkTicker, BUY);
        assert(sellOrderBook.length == 1, "SELL order book should have 1 order")
        assert(buyOrderBook.length == 0, "BUY order book should be empty")
        assert(sellOrderBook[sellOrderBook.length-1].amount == 50, "50 should be remaining to be filled")

        // balances should increase after SELL
        let linkAfterBalances = await getBalances(dex, accounts, linkTicker)
        let ethAfterBalances = await getBalances(dex, accounts, ethTicker)
        let expectedLinkDiffs = [0, 200, 100, 300, -600]
        let expectedEthDiffs = [0, -60000, -20000, -30000, 110000]
        await validateDiffs("LINK", linkPrevousBalances, linkAfterBalances, expectedLinkDiffs)
        await validateDiffs("ETH", ethPrevousBalances, ethAfterBalances, expectedEthDiffs)
    })
    it("should not find matching orders when the prices are too far apart", async () => {
        let dex = await DexMock.deployed()
        let link = await Link.deployed()
        let [alice, bob, chuck, david] = accounts

        await clearBalances(dex, accounts, [linkTicker, ethTicker])
        await dex.clearOrderBook(SELL, linkTicker)
        await dex.clearOrderBook(BUY, linkTicker)

        await createDeposit(alice, bob, ethTicker, 200*300)
        await createDeposit(alice, chuck, linkTicker, 400)

        await dex.createOrder(BUY, linkTicker, 200, 300, {from: bob})
        await dex.createOrder(SELL, linkTicker, 200, 400, {from: chuck})

        let sellOrderBook = await dex.getOrderBook(linkTicker, SELL);
        let buyOrderBook = await dex.getOrderBook(linkTicker, BUY);
        assert(sellOrderBook.length == 1, "SELL order book should have 1 trade")
        assert(buyOrderBook.length == 1, "BUY order book should have 1 trade")
    })
    it("should immediately fail when the last trade amount has not been set and market orders are present on both sides", async () => {
        let dex = await DexMock.deployed()
        let link = await Link.deployed()
        let [alice, bob, chuck, david] = accounts
        await dex.clearLastPrice();
        await dex.clearOrderBook(SELL, linkTicker)
        await dex.clearOrderBook(BUY, linkTicker)

        await createDeposit(alice, bob, linkTicker, 1)
        await createDeposit(alice, chuck, ethTicker, 1)

        await dex.createOrder(SELL, linkTicker, 1, 0, {from: bob})
        await truffleAssert.reverts(
            dex.createOrder(BUY, linkTicker, 1, 0, {from: chuck})
        )
    })
    it("should fail when order for buy/sell of ETH", async () => {
        let dex = await DexMock.deployed()
        let link = await Link.deployed()
        let [alice, bob, chuck, david] = accounts
        await truffleAssert.reverts(
            dex.createOrder(BUY, ethTicker, 1, 1, {from: bob})
        )
        await truffleAssert.reverts(
            dex.createOrder(SELL, ethTicker, 1, 1, {from: chuck})
        )
    })
})

1 Like

code too big to fit in one post. here is the rest:

mock.sol:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;

contract Mock {
    enum EthereumNetwork {
        // Ethereum's mainnet ID is 1
        Unknown, MainNet
    }

    modifier disableOnMainnet() {
        require(getChainId() != uint256(EthereumNetwork.MainNet), "calling test functions on Ethereum's MainNet is not allowed");
        _;
    }

    function getChainId() private view returns (uint256 chainId) {
        assembly {
            chainId := chainid()
        }
    }
}

dexmock.sol:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
pragma experimental ABIEncoderV2;

import "./dex.sol";
import "./mock.sol";

contract DexMock is Dex, Mock {
    event orderBookCleared(Side side, bytes32 ticker, uint removedCount);
    event lastPriceCleared(uint previousPrice);

    function clearOrderBook(Side side, bytes32 ticker) external disableOnMainnet {
        Order[] storage orders = orderBook[ticker][side];
        uint removedCount = orders.length;
        while (orders.length > 0) {
            orders.pop();
        }
        emit orderBookCleared(side, ticker, removedCount);
    }

    function clearLastPrice() external disableOnMainnet {
        uint previousPrice = lastPrice;
        lastPrice = 0;
        emit lastPriceCleared(previousPrice);
    }
}

test results
Contract: DexMock-market
√ for SELL market order, seller needs enough tokens to sell (7058ms)
√ for BUY market order the buyer needs enough ETH for the trade (9104ms)
√ market SELL orders can be submitted even if the BUY order book is empty (3104ms)
√ market BUY orders can be submitted even if the SELL order book is empty (2183ms)
√ market BUY orders should be filled until the SELL order book is empty (21414ms)
√ market SELL orders should be filled until the BUY order book is empty (25474ms)
√ market BUY orders should be filled until the SELL order book is fulfilled (27712ms)
√ market SELL orders should be filled until the BUY order book is fulfilled (26401ms)
√ should not find matching orders when the prices are too far apart (13965ms)
√ should immediately fail when the last trade amount has not been set and market orders are present on both sides (6048ms)
√ should fail when order for buy/sell of ETH (1011ms)

Contract: DexMock
√ user must have enough eth to cover the buy order (2037ms)
√ user should have enough tokens for the sell order (3053ms)
√ buy order book should always be ascending in price (6130ms)
√ sell order book should always be descending in price (5463ms)

Contract: Wallet
√ should only be possible for owners to add tokens (1179ms)
√ should handle deposits/withdrawls correctly (2393ms)

17 passing (3m)

2 Likes

Here is my Dex contract. I plan on adding a front end after completing some of the other courses!

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;
import "../contracts/wallet.sol";

contract Dex is Wallet {

    using SafeMath for uint256;

    event tester(uint test, string where);

    enum Side{
        BUY,
        SELL
    }

    struct Order{
        uint id;
        address trader;
        Side side;
        bytes32 ticker;
        uint amount;
        uint price;
        uint filled;
    }

    uint public nextOrderId = 0;

    mapping(bytes32 => mapping(uint => Order[])) public orderBook;

    function getOrderBook(bytes32 ticker,Side side) view public returns (Order[] memory){
        return orderBook[ticker][uint(side)];

    }

    function createLimitOrder(Side _side, bytes32 _ticker, uint _amount, uint _price) public {
        if(_side == Side.BUY){
            require(balances[msg.sender]["ETH"] >= _amount.mul(_price));
        }
        else if(_side == Side.SELL){
            require(balances[msg.sender][_ticker] >= _amount);
        }
        Order[] storage orders = orderBook[_ticker][uint(_side)];
        orders.push(
            Order(nextOrderId,msg.sender,_side,_ticker,_amount,_price,0)
        );
        if(_side == Side.BUY){
            for ( uint i = orders.length -1; i > 0; i-- ){
                if (orders[i].price < orders[i-1].price){
                    break;
                }
                Order memory tmp  = orders[i];
                    orders[i] = orders[i-1];
                    orders[i-1] = tmp;
            }
        }
        else if(_side == Side.SELL){
            for ( uint i = orders.length -1; i > 0; i-- ){
                if (orders[i].price > orders[i-1].price){
                    break;
                }
                Order memory tmp  = orders[i];
                    orders[i] = orders[i-1];
                    orders[i-1] = tmp;
            }
        }
        nextOrderId++;
    }

    function createMarketOrder(Side _side, bytes32 _ticker, uint _amount) public{

        if(_side == Side.SELL){
            require(balances[msg.sender][_ticker] >= _amount, "Insufficient balance");
        }


        uint orderBookSide;
        if(_side == Side.BUY){
            orderBookSide = 1;
        }
        else{
            orderBookSide = 0;
        }

        Order[] storage orders = orderBook[_ticker][orderBookSide];

        uint totalFilled = 0;
        uint leftToFill = 0;
        uint toremove = 0;
        if(_side == Side.BUY){

            //verify buyer has enough eth to cover the purchase (require)
            uint totaleth;
            for (uint256 i = 0; i< orders.length && totalFilled < _amount; i++){
                uint amountAvailable = orders[i].amount.sub(orders[i].filled);
                
                if ( _amount >= amountAvailable){
                totalFilled = totalFilled.add(amountAvailable);
                totaleth = amountAvailable.mul(orders[i].price);
                    }

                else{
                totalFilled = _amount;
                totaleth = orders[i].price.mul(_amount.sub(totalFilled));
                    }

                require(balances[msg.sender][bytes32("ETH")] >= (totaleth), "Not enough eth to fill order");    
                }

            //reset totalFilled 
            totalFilled = 0;
            for (uint256 i = 0; i< orders.length && totalFilled < _amount; i++){
                leftToFill = _amount.sub(totalFilled);
                //how much we can fill from order[i]
                uint amountAvailable = orders[i].amount.sub(orders[i].filled) ;

                //update totalFilled;
                if ( leftToFill >= amountAvailable){
                    totalFilled = totalFilled.add(amountAvailable);
                    orders[i].filled = orders[i].amount;
                    toremove = toremove.add(1);
                    //emit tester(toremove, "Buy side in if statement");
                    //emit tester(totalFilled, " Total filled, Buy side in if statement");
                    //emit tester(_amount, " _amount, Buy side in if statement");
                    //Execute trade & shift balances between buyer and seller 
                    balances[msg.sender][bytes32("ETH")] = balances[msg.sender][bytes32("ETH")].sub(amountAvailable.mul(orders[i].price));//subtract eth from market buyer
                    balances[orders[i].trader][_ticker] = balances[orders[i].trader][_ticker].sub(amountAvailable); //subtract link from limit seller
                    balances[orders[i].trader][bytes32("ETH")] = balances[orders[i].trader][bytes32("ETH")].add(amountAvailable.mul(orders[i].price));//Add eth to limit seller
                    balances[msg.sender][_ticker] = balances[msg.sender][_ticker].add(amountAvailable); //add link to market buyer
                    }
                else{
                    orders[i].filled = orders[i].filled.add(_amount.sub(totalFilled));
                    totalFilled = _amount;
                    //emit tester(_amount, "_amount value in buy side else statement");
                    //emit tester(orders[i].filled, "order[i].filled value in buy side else statement");
                    //Execute trade & shift balances between buyer and seller 
                    balances[msg.sender][bytes32("ETH")] = balances[msg.sender][bytes32("ETH")].sub(_amount.mul(orders[i].price));//subtract eth from market buyer
                    balances[orders[i].trader][_ticker] = balances[orders[i].trader][_ticker].sub(_amount); //subtract link from limit seller
                    balances[orders[i].trader][bytes32("ETH")] = balances[orders[i].trader][bytes32("ETH")].add(_amount.mul(orders[i].price));//Add eth to limit seller
                    balances[msg.sender][_ticker] = balances[msg.sender][_ticker].add(_amount); //add link to market buyer
                    }
                }
                
            }   
        else{
            for (uint256 i = 0; i< orders.length && totalFilled < _amount; i++){
                leftToFill = _amount.sub(totalFilled);
            //how much we can fill from order[i]
            uint amountAvailable = orders[i].amount.sub(orders[i].filled);

                //update totalFilled;
                if ( leftToFill >= amountAvailable){
                    totalFilled = totalFilled.add(amountAvailable);
                    orders[i].filled = orders[i].amount;
                    toremove = toremove.add(1);
                    //emit tester(toremove, "Sell side in if statement");
                    //Execute trade & shift balances between buyer and seller 
                    balances[orders[i].trader][bytes32("ETH")] = balances[orders[i].trader][bytes32("ETH")].sub(amountAvailable.mul(orders[i].price));//subtract eth from limit buyer
                    balances[msg.sender][_ticker] = balances[msg.sender][_ticker].sub(amountAvailable); //subtract link from market seller
                    balances[msg.sender][bytes32("ETH")] = balances[msg.sender][bytes32("ETH")].add(amountAvailable.mul(orders[i].price));//Add eth to Market seller
                    balances[orders[i].trader][_ticker] = balances[orders[i].trader][_ticker].add(amountAvailable); //add link to market buyer
                    }
                else{
                    orders[i].filled = orders[i].filled.add(_amount.sub(totalFilled));
                    totalFilled = _amount; 
                    //Execute trade & shift balances between buyer and seller 
                    balances[orders[i].trader][bytes32("ETH")] = balances[orders[i].trader][bytes32("ETH")].sub(_amount.mul(orders[i].price));//subtract eth from limit buyer
                    balances[msg.sender][_ticker] = balances[msg.sender][_ticker].sub(_amount); //subtract link from market seller
                    balances[msg.sender][bytes32("ETH")] = balances[msg.sender][bytes32("ETH")].add(_amount.mul(orders[i].price));//Add eth to Market seller
                    balances[orders[i].trader][_ticker] = balances[orders[i].trader][_ticker].add(_amount); //add link to market buyer
                    }
                }
            }
        //shift all of the elements up to front of orderbook
        //emit tester(toremove, "before removing elements");
        for ( uint i = toremove; i < orders.length; i++ ){
                orders[i-toremove] = orders[i];
            }
        //delete elements from the end of orderbook 
        for ( uint i = 0; i < toremove ; i++ ){
                orders.pop();
            }


        //Loop through the orderbook and remove 100% filled orders

    }









}
2 Likes

@wleskay thats deadly g. Do old smart contract programming 201 next if you want to experience linking backend and front end. the final project in that is much tougher than this because filip doesnt give you any code or solution walkthroughs and you get thrown straight into the deep end for making a front end and figuring out how to connect it to you contract code. its great fun youll learn lodes. Im just finisng it up now took me me like a week and a half of flat out full 7/8 hour coding days to finish it. taught me so much though. Another great course is the eth game programming if you havent alrready taken it.

1 Like

Hi All,

Finally, the moment, when I have managed to complete the Dex project. It is looong and arduous :sweat:

Huge amount of learning.

Thanks a lot IvanOnTech, specially @filip, @dani and @mcgrane5 for all the help.

My project is fully updated in Github:

https://github.com/rays70/DEX

Best Regards
Subhasis

2 Likes

hey @rays deadly man. I applaud your work for doing it your own way, Great work a unique approach it surely is the best way to learn lodes im a huge fan of this. quickly glimpsed at your code there very well done for trynna mke your own sorting and the execute trade function. I’d say you learned lodes you should try sol 201 [OLD] it will give you great skills required for front end purposes which you could use to make a cool front end for this DEX. Im currently in the process of taking the react course for this very purpose. That brilliant g now onto the next course, whatever that may be!!

Evan

1 Like

thanks a lot @mcgrane5 for your kind words and all the help :grinning:

Will check out the old sol 201…

Cheers !

2 Likes

Hello,

I’ve been goofing around with the DEX a bit the past week, and I’ve come up against these “unreachable code” messages when I “truffle compile”:

,Warning: Unreachable code.
   --> /C/Users/davem/Coding/IvanAcademy/DEX/contracts/dex.sol:145:17:
    |
145 |                 removeOrderFromBook(thisSellOrder);
    |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

,Warning: Unreachable code.
   --> /C/Users/davem/Coding/IvanAcademy/DEX/contracts/dex.sol:146:17:
    |
146 |                 amountToExchange -= TESTING_thisAmountExchanged;
    |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

,Warning: Unreachable code.
   --> /C/Users/davem/Coding/IvanAcademy/DEX/contracts/dex.sol:151:17:
    |
151 |                 thisSellOrder.amount = thisSellOrder.amount.sub(isPartialFill);
    |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

,Warning: Unreachable code.
   --> /C/Users/davem/Coding/IvanAcademy/DEX/contracts/dex.sol:152:17:
    |
152 |                 removeOrderFromBook(marketOrderBeingFilled);
    |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

,Warning: Unreachable code.
   --> /C/Users/davem/Coding/IvanAcademy/DEX/contracts/dex.sol:153:17:
    |
153 |                 amountToExchange -= isPartialFill;
    |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Can anyone clue me in to why I’m getting this error? Below is the code pertinent to the function throwing the warning. I’ve been experimenting with how I use scope and memory location, I think it has to do with that, but I’m not sure yet.

contract Dex is Wallet{
 
    using SafeMath for uint256;

    enum Side{
    BUY,
    SELL
    }
    
    struct Order{
        uint id;
        address trader;
        Side side;
        string ticker;
        uint amount;
        uint price;
        uint amountSpokenFor;
        uint isMarketOrder;
    }
        
    uint public orderIds = 0;

    mapping(string => mapping(uint => Order[])) public orderBook;

function executeNewMarketBuyOrder(string memory ticker, uint buyOrderBookLength) internal{
        Order[] storage sellOrders = orderBook[ticker][1];
        Order memory marketOrderBeingFilled = getOrderBook(ticker, Side.BUY)[buyOrderBookLength - 1];
        uint amountToExchange = marketOrderBeingFilled.amount;
        uint isPartialFill;
        uint TESTING_thisAmountExchanged = 0;
        Order memory thisSellOrder = orderBook[ticker][1][0];
        while(amountToExchange > 0){
            //Order memory thisSellOrder = orderBook[ticker][1][0];
            if(thisSellOrder.amount == thisSellOrder.amountSpokenFor){
                isPartialFill = 0;
                TESTING_thisAmountExchanged = thisSellOrder.amountSpokenFor;
                transferTokenInDex(thisSellOrder, marketOrderBeingFilled, isPartialFill);
                removeOrderFromBook(thisSellOrder);
                amountToExchange -= TESTING_thisAmountExchanged;
            }
            else{
                isPartialFill = thisSellOrder.amountSpokenFor;
                transferTokenInDex(thisSellOrder, marketOrderBeingFilled, isPartialFill);
                thisSellOrder.amount = thisSellOrder.amount.sub(isPartialFill);
                removeOrderFromBook(marketOrderBeingFilled);
                amountToExchange -= isPartialFill;
            }
        }
    }

function transferTokenInDex(Order memory tokenSellersOrder, Order memory tokenBuyersOrder, uint isPartialFill) internal{
        uint ethTransferAmount = isPartialFill != 0 ? isPartialFill*tokenSellersOrder.price : tokenSellersOrder.amount*tokenSellersOrder.price;
        uint tokenTransferAmount = isPartialFill != 0 ? isPartialFill: tokenSellersOrder.amount;
        require(balances[tokenSellersOrder.trader][tokenSellersOrder.ticker] >= tokenSellersOrder.amount, "You don't have the funds to make this transfer");
        require(balances[tokenBuyersOrder.trader]["ETH"] >= ethTransferAmount, "you don't have enough ETH for this trade");
        
        balances[tokenSellersOrder.trader][tokenSellersOrder.ticker] = balances[tokenSellersOrder.trader][tokenSellersOrder.ticker].sub(tokenTransferAmount);
        balances[tokenBuyersOrder.trader][tokenBuyersOrder.ticker] = balances[tokenBuyersOrder.trader][tokenBuyersOrder.ticker].add(tokenTransferAmount);
        balances[tokenSellersOrder.trader]["ETH"] = balances[tokenSellersOrder.trader]["ETH"].add(ethTransferAmount);
        balances[tokenBuyersOrder.trader]["ETH"] = balances[tokenBuyersOrder.trader]["ETH"].sub(ethTransferAmount);
    }

function removeOrderFromBook(Order memory orderToRemove) internal returns(bool){
        Order[] storage activeOrderBook = orderBook[orderToRemove.ticker][uint(orderToRemove.side)];
        if(orderToRemove.id == activeOrderBook[activeOrderBook.length - 1].id){//if order is at the end of the book, just pop 
            activeOrderBook.pop();
            return true;
        }
        else{
            for(uint i = 0; i < activeOrderBook.length; i++){
                activeOrderBook[i] = activeOrderBook[i+1];
            }
            activeOrderBook.pop();
            return true;
        }
    }
}

Any insight would hasten the arrival of my epiphany.

Respectfully,
David

Hi @Melshman

The compiler is telling you that the functions you are calling cannot be reached because the code execution will terminate before those specific lines of code are executed.

Check the logic of your code, if you cannot spot the error please push your project to github and share the link.

Cheers,
Dani

Hi,
finally added some little frontend to the dex contract. It is really a nice project and a lot of fun. Going on for the next project.
Portfolio TradingView

https://github.com/henmeh/DEX_Project
Thank you IvanOnTech-Team for the great work.

Best regards
Henning

3 Likes

All gas fees are paid from your web3 wallet (such as Metamask), not from your balance mapping on the DEX. When you deposit ETH to the DEX you are transferring ETH to the contract itself, and the contract tracks your portion of its balance via the mapping, but you have to leave some ETH behind in your web3 wallet to pay for the gas fees. Every time you interact with the DEX your Metamask will pop-up with the transaction confirmation, which will tell you how much gas you owe. The gas fees are paid with ETH that is still in your personal wallet, not the DEX’s wallet, so there’s no need to calculate gas fees in the DEX’s coding.

I hope that explains it, it’s a good newbie question and it totally deserves an answer :slight_smile:

1 Like

hey @henmeh Man this is so cool i went onto your github and used it myself. Amazing. Im currently working on a front end for this myself and man yours is giving me major motivation. Love the way your using morallis too started dabbling in that recently have been following the rarible clone series and other videos. Quick and easy user auth. hahah it shows to on your frontend too soo good. Man amazing piece of work very skilled.

1 Like