Project - DEX Final Code

New feature: cancel limit order

dex.sol

function cancelLimitOrder(Side _side, bytes32 _ticker) public {
  Order[] storage orders = orderBook[_ticker][uint8(_side)];
  bool didOrder = false;
  
  //Delete cancelled orders and shift orderbook
  for(uint i=0; i < order.length; ){
    if(orders[i].trader == msg.sender){
      didOrder = true;
      for(uint j=i; j<orders.length-1; j++){
        orders[j] = orders[j+1];
      }
      orders.pop();
    }else{
      i++;
    }
  }
  require(didOrder, "No orders were submitted");
}

limitOrderTest.js

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

const LINK = web3.utils.fromUtf8("LINK");

contract("Dex", accounts => {
  let dex;
  let link;
  let orderbook;

  before(async function(){
    dex = await Dex.deployed();
    link = await Link.deployed();
    await dex.addToken(LINK, link.address, {from: accounts[0]});
  });
  
  it("Order has to be submitted before cancelling the order", async () => {
    await dex.cleanOrderBook(LINK, 1);
    await truffleAssert.reverts(
      dex.cancelLimitOrder(1, LINK)
    );
  });

  it("Cancelled trader's order should not remain in the orderbook", async () => {
    await dex.createLimitOrder(1, LINK, 1, 300);
    await dex.createLimitOrder(1, LINK, 1, 100);
    await dex.createLimitOrder(1, LINK, 1, 200);
    
    dex.cancelLimitOrder(1, LINK)
    orderbook = await dex.getOrderBook(LINK, 1);
    assert.equal(orderbook.length, 0);
  });

  it("Other account's order should remain, in the right order", async () => {
    orderbook = await dex.getOrderBook(LINK, 1);
    assert(orderbook.length == 0, "The SELL orderbook should be empty");

    //Send LINK tokens to accounts 1, 2, 3 from 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: accounts[2]});
    await link.approve(dex.address, 50, {from: accounts[3]});
    //Deposit LINK into DEX for accounts 1, 2, 3
    await dex.deposit(50, LINK, {from: accounts[1]});
    await dex.deposit(50, LINK, {from: accounts[2]});
    await dex.deposit(50, LINK, {from: accounts[3]});
    //Fill up the sell order book
    await dex.createLimitOrder(1, LINK, 5, 100, {from: accounts[1]});
    await dex.createLimitOrder(1, LINK, 5, 200, {from: accounts[2]});
    await dex.createLimitOrder(1, LINK, 5, 400, {from: accounts[2]});
    await dex.createLimitOrder(1, LINK, 5, 300, {from: accounts[3]});

    dex.cancelLimitOrder(1, LINK, {from:accounts[2]});
    orderbook = await dex.getOrderBook(LINK, 1);
    assert(
      orderbook[0].price == 100 &&
      orderbook[0].trader == accounts[1] &&
      orderbook[1].price == 300 &&
      orderbook[1].trader == accounts[3] 
    );
  });

});


2 Likes

I am gonna continue learning with pleasure. :smiley:

Thanks for your support @Dani.

1 Like

My repository: https://github.com/shawnwollenberg/Solidity_DEX_Project
I look forward to learning how to add a front end. Seeing the pain-points from a user perspective should help me figure out ways to make improvements to the backend. Another great course!!! Thank you!

2 Likes

So for my DEX I created the ERC20 token that will be further expanded upon for my company. We are looking to create an Alternative Medicine Telehealth directory and a digital apothecary fortified by the Blockchain called DECENT MED (DMED).
DEX:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;
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 = 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)
        );
    //bubble sort
    uint i = orders.length > 0 ? orders.length -1:0;
        if(side==Side.BUY){
            while(i>0){
                if(orders[i-1].price > orders[i].price){
                    break;
                }
                Order memory orderToMove = orders[i-1];
                orders[i-1] = orders[i];
                orders[i] = orderToMove;
                i--;
            }
        } else if(side == Side.SELL) {
    while(i>0){
        if(orders[i - 1].price < orders[i].price){
            break;
        }
        Order memory orderToMove = orders[i+1];
        orders[i-1] = orders[i];
        orders[i] = orderToMove;
        i--;
        }
    } 
    nextOrderId++;  
 }

        function createMarketOrder(Side side, bytes32 ticker, uint amount) external {
            if(side == Side.SELL) {
                require(balances[msg.sender][ticker]>= amount);
            }
           uint orderBookSide; 
           if(side == Side.BUY) {
               orderBookSide = 1;
           }
            else{
                orderBookSide = 0;
            }
            Order[] storage orders = orderBook[ticker][orderBookSide];
            uint totalFilled;
            
            for(uint256 i = 0; i<orders.length && totalFilled<amount; i++) {
                uint leftToFill = amount.sub(totalFilled);
                uint availableToFill = orders[i].amount.sub(orders[i].filled);
                uint filled = 0;
                if(availableToFill > leftToFill) {
                    filled = leftToFill;
                } else {
                    filled = availableToFill;
                }
                totalFilled = totalFilled.add(filled);
                orders[i].filled = orders[i].filled.add(filled);
                uint cost = filled.mul(orders[i].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][ticker].sub(cost);
                
                balances[orders[i].trader][ticker] = balances[orders[i].trader][ticker].sub(filled);
                balances[orders[i].trader]["ETH"] = balances[orders[i].trader][ticker].add(cost);

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

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


}

Dmed.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../@openzeppelin/contracts/utils/math/SafeMath.sol";

contract DMED is ERC20 {
    using SafeMath for uint256;
    constructor () ERC20("DecentMed", "DMED") {
        _mint(msg.sender, 1000);
    }
    function isContract(address account) internal view returns(bool) {
        uint256 size;
            // solhint-disable-next-line no-inline-assembly
            assembly { size := extcodesize(account) }
            return size > 0;
    }
}

Wallet.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../@openzeppelin/contracts/utils/math/SafeMath.sol";
import "../@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;

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

    function deposit(uint amount, bytes32 ticker) external tokenExist(ticker) {
        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) external tokenExist(ticker) {
        require(balances[msg.sender][ticker] >= (amount));
        balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(amount);
        IERC20(tokenMapping[ticker].tokenAddress).transfer(msg.sender, amount);
    }
    function balance(bytes32 ticker) external view{
        balances[msg.sender][ticker];
    }
        function depositEth() payable external {
        balances[msg.sender][bytes32("ETH")] = balances[msg.sender][bytes32("ETH")].add(msg.value);
    }  
    function withdrawEth(uint amount) external {
        require(balances[msg.sender][bytes32("ETH")] >= amount);
        balances[msg.sender][bytes32("ETH")] = balances[msg.sender][bytes32("ETH")].sub(amount);
        
    }
      modifier tokenExist(bytes32 ticker) {
require(tokenMapping[ticker].tokenAddress != address(0));
_;

}
}

Here is the link to the rest of the code:
https://github.com/THEMERLINGROUP/DMED-via-DEX

1 Like

Finally done with this project!!

I was pushing myself into not watching the help videos and just trying to make it work with the market order, the sorting of the order book and the logics behind it. It feels so great to have done this :smiley:

I think the “next step” for this dex would ofcourse be to build a front end to this and also create functionality for limit orders acting as market orders.

Example:

SELL: 1:200 wei, 1:300 wei

BUY: 2:200 wei

The BUY limit order then should be able to buy one token from the first seller in this example :slight_smile:

I am super excited to begin with the course Ethereum Smart Contract Security to keep on getting better :raised_hands:

dex.sol:

pragma solidity 0.8.0;
pragma experimental ABIEncoderV2;

import "./wallet.sol";

contract Dex is Wallet {
    using SafeMath for uint256;

    //This enum is created for differentiating which side you are trading on (BUY or SELL)
    enum Side {
        BUY,
        SELL
    }

    //This is an order struct which contains several attributes used through the contract
    struct Order {
        uint id;
        address trader;
        Side side;
        bytes32 ticker;
        uint amount;
        uint price;
        uint filled;
    }

    //This is used for adding id numbers to limit orders
    uint public nextOrderId = 0;

    //This is for mapping the orderbook of a specific token and which side (BUY or SELL) it is
    mapping(bytes32 => mapping(uint => Order[])) public orderBook;

    //This is created for depositing ETH and stores the ETH in the balance mapping
    function depositEth() public payable {
        require(0 < msg.value, "Dex: Value needs to be over 0");
        balances[msg.sender][bytes32("ETH")] = balances[msg.sender][bytes32("ETH")].add(msg.value);
    }

    /*
    * This function gets the order book for a specific side (SELL or BUY)
    */
    function getOrderBook(bytes32 ticker, Side side) view public returns(Order[] memory) {
        return orderBook[ticker][uint(side)];
    }

    /*
    * This function is for adding limit orders to the order book based on if it is a BUY or SELL order.
    *
    * The token needs to be added by the contract owner before being able to create limit orders.
    * 
    * If it is a BUY order then the eth balance is checked and if it is a sell order then the 
    * token balance is checked.
    * 
    * The order gets added and then if the orders are unsorted the function sorts them in correct order.
    */
    function createLimitOrder(Side side, bytes32 ticker, uint256 amount, uint price) tokenExist(ticker) public {
        require(0 < amount, "Dex: Amount needs to be higher than 0");

        //Checks of token balance depending on if it is a BUY or SELL order
        if(side == Side.BUY){
        require(amount.mul(price) <= balances[msg.sender]["ETH"], "Dex: Eth balance needs to be higher than the order value");
        }
        else if(side == Side.SELL) {
        require(amount <= balances[msg.sender][ticker], "Dex: Token balance needs to be higher than or equal to the sell order amount");
        }
        
        //Stores the order book in a variable and pushes the order in the array.
        Order[] storage orders = orderBook[ticker][uint(side)];
        orders.push(
            Order(nextOrderId, msg.sender, side, ticker, amount, price, 0)
        );

        //Depending on if it is a BUY or SELL order then the order gets sorted in the correct order
        if(side == Side.BUY){
            for(uint i=1;i<orders.length;i++){
                Order memory moveOrder;
                moveOrder = orders[orders.length-i];

                if(orders[orders.length-i-1].price < moveOrder.price){
                    orders[orders.length-i] = orders[orders.length-i-1];
                    orders[orders.length-i-1] = moveOrder;
                }
                else if(orders[orders.length-i-1].price >= moveOrder.price){
                    break;
                }
            }
        }
        else if(side == Side.SELL){
            for(uint i=1;i<orders.length;i++){
                Order memory moveOrder;
                moveOrder = orders[orders.length-i];

                if(orders[orders.length-i-1].price > moveOrder.price){
                    orders[orders.length-i] = orders[orders.length-i-1];
                    orders[orders.length-i-1] = moveOrder;
                }
                else if(orders[orders.length-i-1].price <= moveOrder.price){
                    break;
                }
            }
        }

        //This variable gets incremented for the next orders
        nextOrderId++;

    }

    /*
    * This function is for making market orders on the dex.
    * 
    * The token needs to be added by the contract owner before being able to create limit orders.
    * 
    * If it is a BUY order then the ETH balance of the total order cost is checked.
    * Example: 3 orders in order book for a total of 8000 wei, msg.sender has created a market 
    * order but only has 7000 wei. Then the order is reverted due to that the total eth price
    * for the trade costs more than the owned amount.
    *
    * If it is a SELL order then the token balance of the order is checked.
    * 
    * The orders that is 100 % filled also gets cleaned by the internal function "cleanOrderBook".
    */
    function createMarketOrder(Side side, bytes32 ticker, uint amount) tokenExist(ticker) public{
        uint orderBookSide;
        if(side == Side.BUY){
            orderBookSide = 1;
            //Checks if the buyer has enough ETH balance for the trade
            checkEthBalance(ticker, amount);
        }
        else{
            orderBookSide = 0;
            //Checks that the msg.sender has sufficient token balance to sell
            require(balances[msg.sender][ticker] >= amount, "Insufficient balance");
        }

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

        uint totalFilled;

        /*
        * Loops through the entire orderbook and shifts balances with as many orders possible for 
        * the market order.
        * 
        * After the loop is completed then the order book gets cleaned up.
        */
        for (uint256 i = 0; i < orders.length && totalFilled < amount; i++) {
            //Checks the amount that can be filled of the current order
            uint indexAmountToFill = orders[i].amount.sub(orders[i].filled);
            uint indexPrice;
            
            //Checks if the amount left to fill is less than or equal than the amount to fill
            if(amount.sub(totalFilled) <= indexAmountToFill){
                //Updates the filled attribute in the order
                orders[i].filled = orders[i].filled.add(amount.sub(totalFilled));
                //Total price of the amount to fill and the order's price
                indexPrice = orders[i].price.mul(amount.sub(totalFilled));

                //Shifts the balances between buyer/seller
                shiftBalances(orders[i].trader, ticker, orderBookSide, indexPrice, indexAmountToFill);
                //Update of the totalFilled variable (used for the for loop)
                totalFilled = totalFilled.add(amount.sub(totalFilled));
            }
            else{
                orders[i].filled = orders[i].filled.add(indexAmountToFill);
                indexPrice = orders[i].price.mul(indexAmountToFill);

                //Shifts the balances between buyer/seller
                shiftBalances(orders[i].trader, ticker, orderBookSide, indexPrice, indexAmountToFill);
                totalFilled = totalFilled.add(indexAmountToFill);
            }
            
        }

        //Loops through the orderbook and removes the 100% filled orders
        cleanOrderBook(orderBookSide, ticker);
        
    }

    //This function is created for shifting the balances between buyer and seller in a market order
    function shiftBalances(address fromTrader, bytes32 ticker, uint orderBookSide, uint indexPrice, uint indexAmountToFill) internal{
        if(orderBookSide == 1){
            balances[msg.sender][ticker] = balances[msg.sender][ticker].add(indexAmountToFill);
            balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].sub(indexPrice);

            balances[fromTrader][ticker] = balances[fromTrader][ticker].sub(indexAmountToFill);
            balances[fromTrader]["ETH"] = balances[fromTrader]["ETH"].add(indexPrice);
            
        }
        else{
            balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(indexAmountToFill);
            balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].add(indexPrice);

            balances[fromTrader][ticker] = balances[fromTrader][ticker].add(indexAmountToFill);
            balances[fromTrader]["ETH"] = balances[fromTrader]["ETH"].sub(indexPrice);
            
        }
    }

    //This function is created for checking ETH balance on a market BUY order
    function checkEthBalance(bytes32 ticker, uint amount) view internal{
        Order[] memory orders = orderBook[ticker][1];
        uint totalEthPrice;
        uint totalAmountToTrade;

        /*
        * Loops through the entire orderbook for up to the the market order amount
        * gets calculated with the existing orders and the amount that exists.
        * 
        * The price is added for each iteration of the loop to totalEthPrice and
        * the balance of msg.sender gets checked at the end of the loop.
        */
        for (uint256 u = 0; u < orders.length && totalAmountToTrade < amount; u++) {
            uint indexTradeableAmount = orders[u].amount.sub(orders[u].filled);

            if(amount.sub(totalAmountToTrade) <= indexTradeableAmount){
                totalEthPrice = totalEthPrice.add(orders[u].price.mul(amount.sub(totalAmountToTrade)));
                totalAmountToTrade = totalAmountToTrade.add(amount.sub(totalAmountToTrade));
            }
            else{
                totalEthPrice = totalEthPrice.add(orders[u].price.mul(indexTradeableAmount));
                totalAmountToTrade = totalAmountToTrade.add(indexTradeableAmount);
            }
        }

        //Checks if the ETH balance of msg.sender is higher than the total cost
        require(balances[msg.sender]["ETH"] >= totalEthPrice, "Not enough ETH balance for the trade");
    }

    //This function is created for checking the orderbook and cleaning the filled orders
    function cleanOrderBook(uint orderBookSide, bytes32 ticker) internal{
        Order[] storage orders = orderBook[ticker][orderBookSide];
        
        /*
        * First this checks if the current index order is 100 % filled.
        * 
        * For each time it is a 100 % filled order then it gets replaced, the loop gets sorted and the last array index gets poped
        */
        for(uint256 i = 0; i < orders.length; i) {
            if(orders.length == 1){
                if(orders[i].amount == orders[i].filled){
                    orders.pop();
                }
                else{
                    break;
                }
            }

            else if(orders[i].amount == orders[i].filled){
                Order memory moveOrder;
                for(uint256 u = 0; u < orders.length - 1; u++){
                    moveOrder = orders[u+1];
                    orders[u] = moveOrder;
                }
                orders.pop();
            }
            
            else{
                break;
            }
        }
    }
    
}

wallet.sol:

pragma solidity 0.8.0;

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;
    }

    //This mapping shows an added token and its address.
    mapping(bytes32 => Token) public tokenMapping;
    bytes32[] public tokenList;
    
    //This is a mapping for displaying the total amount of a token a specific address has.
    mapping(address => mapping (bytes32 => uint256)) public balances;

    //This modifier can be used to check if the token is added to the exchange or not.
    modifier tokenExist(bytes32 ticker){
        require(tokenMapping[ticker].tokenAddress != address(0), "Wallet: Token does not exist");
        _;
    }

    /*
    * This adds the token to the exchange.
    * 
    * Only the owner of the contract is able to add tokens.
    */
    function addToken(bytes32 ticker, address tokenAddress) onlyOwner external {
        tokenMapping[ticker] = Token(ticker, tokenAddress);
        tokenList.push(ticker);
    }

    /*
    * Created for depositing a ERC20-token.
    * 
    * This token needs to be added before it can be deposited through the "addToken"-function.
    */
    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);
    }

    /*
    * This is for withdrawing a token.
    * 
    * First the function is checking if the token exists. Then it checks
    * if the msg.sender has the balance of the specified token.
    */
    function withdraw(uint amount, bytes32 ticker) tokenExist(ticker) external {
        require(amount <= balances[msg.sender][ticker], "Wallet: Balance not sufficient");

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

market_order_test.js:

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


contract("Dex", accounts => {
    //When creating a SELL market order, the seller needs to have enough tokens for the trade.
    it("should throw an error if token balance is too low when creating SELL market order", async () => {
        let dex = await Dex.deployed()
        
        let balance = await dex.balances(accounts[0], web3.utils.fromUtf8("LINK"))
        assert.equal( balance.toNumber(), 0, "Initial LINK balance is not 0" )

        await truffleAssert.reverts(
            dex.createMarketOrder(1, web3.utils.fromUtf8("LINK"), 10000)
        )
    })
    //When creating a BUY market order, the buyer needs to have enough ETH for the trade.
    it("should throw an error if ETH balance is too low when creating market BUY order", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()

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

        //Token "LINK" added
        //5 tokens approved for accounts[0]
        //5 LINK deposited
        await dex.addToken(web3.utils.fromUtf8("LINK"), link.address)
        await link.approve(dex.address, 5)
        await dex.deposit(5, web3.utils.fromUtf8("LINK"))

        //Sell limit: 5 LINK, 100 wei per token
        await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 5, 100)

        await truffleAssert.reverts(
            dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 7)
        )
        
        await dex.depositEth({value: 500})
        
        /*
        * To remove the sell limit order.
        * Market BUY: 5 LINK for 500 wei
        */
        await dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 5)
    })
    //Market orders can be submitted even if the order book is empty
    it("should not throw an error if order book is empty when creating market order", async () => {
        let dex = await Dex.deployed()

        await dex.depositEth({value: 10000})

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

        await truffleAssert.passes(
            dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 10)
        )
    })
    //Market orders should 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)
        assert(orderbook.length == 0, "Sell side Orderbook should be empty at start of test")

        //Token (LINK) is already added

        //Send LINK tokens to accounts 1, 2, 3 from accounts 0
        await link.transfer(accounts[1], 50)
        await link.transfer(accounts[2], 50)
        await link.transfer(accounts[3], 50)

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

        //Deposit 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/3 orders in the book
        await dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 10)

        orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1)
        assert(orderbook.length == 1, "Sell side Orderbook should only have 1 order left")
        assert(orderbook[0].filled == 0, "Sell side order should have 0 filled")
                
    })
    
    //Market orders 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)
        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 buyer link balance before link purchase
        let balanceBefore = await dex.balances(accounts[0], web3.utils.fromUtf8("LINK"))

        //ETH balance is 6500. Adding another 10000, the sum is 16500.
        await dex.depositEth({value: 10000})

        //Create market order that could fill more than the entire order book (15 link)
        await dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 50)

        //check buyer link balance after link purchase
        let balanceAfter = await dex.balances(accounts[0], web3.utils.fromUtf8("LINK"))

        //Buyer should have 15 more link after, even tough order was for 50
        assert.equal(balanceBefore.toNumber() + 15, balanceAfter.toNumber())

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

        //Seller deposits link and creates a sell limit order for 1 link for 300 wei
        await link.approve(dex.address, 500, {from: accounts[1]})
        await dex.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 - 300, balanceAfter)
    })
    //The token balances of the limit order sellers should decrease with the filled amounts.
    it("token balance of the limit order seller should decrease with the filled amount", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()

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

        //Seller Account[1] already has approved and deposited Link

        //Transfer 100 LINK to accounts[2]
        await link.transfer(accounts[2], 100)

        //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 sellers Link balances before trade
        let account1balanceBefore = await dex.balances(accounts[1], web3.utils.fromUtf8("LINK"))
        let account2balanceBefore = await dex.balances(accounts[2], web3.utils.fromUtf8("LINK"))

        //Account[0] created market 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 limit orders should be removed from the orderbook", async () => {
        let dex = await Dex.deployed()

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

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

        orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1)
        assert(orderbook.length == 0, "Sell side Orderbook should be empty after trade")
    })
    //Partly filled limit orders should be modified to represent the filled/remaining amount
    it("Limit orders filled property should be set correctly after a trade", async () => {
        let dex = await Dex.deployed()

        let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1)
        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)
        assert.equal(orderbook[0].filled, 2)
        assert.equal(orderbook[0].amount, 5)
    })
    
})
1 Like

Here is my code:

https://github.com/danielmain/decentralized-exchange

project structure:

  • ./contracts/Dex.sol
  • ./contracts/Wallet.sol
  • ./contracts/Link.sol
  • ./test/Dex.test.js
1 Like

My code for the dex can be found on GitHub:

https://github.com/CatalystJesal/dex

I may improve this further by adding a front-end and use Moralis for it. Otherwise, I’ll work on a different project and start learning Moralis. I’ll now take a well-deserved 1-2 weeks break while job searching before doing all those things, been overworked with professional job and academy over the past 7 months non-stop. :joy:

Anyways, great job on the content as always, appreciate it. Keep it up!

2 Likes

Would be awesome if you can show the same project on moralis :slight_smile:

Carlos Z

1 Like

Hey guys,
I am trying to build the frontend for this Dex and my console is throwing this error “Uncaught TypeError: Cannot read property ‘enable’ of undefined”. Ive been searching online for help on this matter but nothing seems to be working. It’s clear that its a jQuery issue. I have installed the latest version and the syntax hasn’t, from my understanding, changed in any way so I’m clueless.

This issue also arose during the CryptoKitties Dapp course when implementing the web3 frontend and i still haven’t managed to solve it. I am using MacOS (incase its necessary but doubt it).

Screenshot 2021-05-08 at 18.39.53
Screenshot 2021-05-08 at 18.39.29

My GitHub repository link:
https://github.com/olfrank/DEX.git

Any help with this would be very much appreciated!!

1 Like

Here is my code: https://github.com/guibvieira/DEX-CryptoProject

1 Like

Hi @ol_frank

I’ve tried to use your front end and I do not get the error message you’ve reported.
Can you please tell me how I can replicate the issue?

Screenshot 2021-05-10 at 09.29.10

Cheers,
Dani

1 Like

@dan-i Thanks for taking a look. I am very confused then. The jQuery is the up-to-date version and the syntax is correct so it shouldn’t throw the error. But every time i write window.ethereum.enable() it never seems to run that line correctly. I’m using Brave browser, could this be the issue?
Screenshot 2021-05-10 at 10.07.02

Here is my dex. Will add “challanges” solutions at later stage.

https://github.com/mars-hub/solidity-dex

1 Like

Hey @ol_frank

Are you sure you haven’t modified anything after you pushed your code to github?
Try the following:

  • Navigate in your project root directory;
  • Run git stash
  • Now python -m SimpleHTTPServer
  • Go to localhost and check if you still get the warning.

If you do, please push again to github.

Let me know,
Dani

1 Like

@dan-i, after i run the python web server nothing happens, am i doing this right??
Screenshot 2021-05-11 at 11.33.24

Hi @ol_frank

Now you can open localhost and check.
Uhmm I hoped for git stash to revert something but did not, let me know.

1 Like

how do you open local host, do you just click on the html file in the folder? @dan-i, sorry for the nooby questions haha, ive never used python web server before

Hi Bhujanga,

question:
did you take the React JS for Web Development course any other courses?

1 Like

Hi @ol_frank

If you were not using a server we found the reason why you were getting the error “cannot read property enable of undefined” :slight_smile:

Once you run the server you have to open localhost:

  • open your browser
  • navigate to localhost:8000

cheers,
Dani

1 Like

Yes I did take the React JS course and several others as well.

2 Likes