Project - DEX Final Code

Please post your final project code in this forum topic.

5 Likes

Here is my DEX.
I wanted to make the challenge a little harder so I make my own sorting algorithm.
Let me know what you think.

Wallet code

Wallet code
pragma solidity >= 0.6.0 < 0.8.0;

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

contract Wallet is Ownable{
    using SafeMath for uint256;
    
    struct Token {
        bytes32 ticker;
        address tokenAddress;
    }

    mapping (address => mapping (bytes32 => uint256)) public balances;
    bytes32[] public tokenList;
    mapping (bytes32 => Token) public tokenMapping;

    /* 
        Checks so that the token is added to the DEX
     */
    modifier tokenExist(bytes32 ticker){
        require(tokenMapping[ticker].tokenAddress != address(0), "Ticker not found");
        _;
    }
    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 remote = IERC20(tokenMapping[ticker].tokenAddress);
        require(remote.balanceOf(msg.sender) >= amount, "Balance not sufficient"); 
        balances[msg.sender][ticker] = balances[msg.sender][ticker].add(amount);   
        remote.transferFrom(msg.sender, address(this), amount);
    }

    function withdraw(uint amount, bytes32 ticker) external tokenExist(ticker){
        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);
    }
}

DEX Contract

DEX contract
pragma solidity >= 0.6.0 < 0.8.0;
pragma experimental ABIEncoderV2;

import "./Wallet.sol";

contract Dex is Wallet {
    using SafeMath for uint256;

    /* Define a side enum for the order books */
    enum Side {
        BUY,
        SELL
    }

    /* Incremental counter for the next order id */
    uint public nextOrderId = 0;

    /* Defines a order that will be put in the order book */
    struct Order {

        /* Id of the order */
        uint id;

        /* The address of the user that creted the order */
        address trader;

        /* The side of the order (BUY/SELL order) */
        Side side;

        /* The ticker of the order (ADA, BTC, ETH and so on) */
        bytes32 ticker;

        /* The amount of the ticker that the user wants to sell or buy */
        uint amount;

        /* The unit price of the ticker that the user wants to sell or buy by */
        uint price;

        /* How much of ther order has been filled */
        uint filled;
    }

    /* BUY and SELL order books for different Tickers */
    mapping(bytes32 => mapping (uint => Order[])) public orderBook;

    /* Gets the order book for the given ticker and side */
    function getOrderBook(bytes32 ticker, Side side) view tokenExist(ticker) public returns (Order[] memory){
        return orderBook[ticker][uint(side)];
    }

    /* Creates a limit order and places it in the relevant order book
        Ticker: The ticker that the order should be associated with
        Side: if the order should be a buy or sell order
        Amount: How many units of the ticker that the user wants to sell or buy
        Price: The unit price that the user wants to place the order on
     */
    function createLimitOrder(bytes32 ticker, Side side, uint amount, uint price) tokenExist(ticker) public {

        // In case it is a buy order, make sure that the user has enough funds to complete the purchase
        if (side == Side.BUY){
            require(balances[msg.sender]["ETH"] >= amount.mul(price), "Not enough ETH");
        } 
        // In case it is a sell order, make sure that the user have enough tokens for the order to go through
        else if (side == Side.SELL){
            require(balances[msg.sender][ticker] >= amount,"Not enough funds");
        }

        // Reference to the relevant order book
        Order[] storage orders = orderBook[ticker][uint(side)];

        // Create and push the order to the order book
        orders.push(Order(nextOrderId, msg.sender, side, ticker, amount, price, 0));

        // Increment the id counter for orders
        nextOrderId++;

        // Define flag that indicates if the order book should be sorted in descending order (true for BUY, false for SELL)
        bool descending = side == Side.BUY;

        // Flag to indicate that a perfect insertion point has been found
        bool found;

        // The index of the perfect insertion point
        uint replaceIndex;

        /* 
            Since this is the only method that pushes orders to the order book we can now guarantee that the order except the last index is already sorted.
            So what we need to do is to find the index where the newly created order belongs in the list.
            We start by looping through the list
         */
        for(uint i = 0; i< orders.length; i++){
            // If we are sorting in descending order AND the price of the current element is lower or equal to the new order
            // OR if we are sorting in ascending order AND the price of the current element is greater or equal to the new order             
            if ((orders[i].price <= orders[orders.length-1].price && descending) || (orders[i].price >= orders[orders.length-1].price && !descending)){
                // Then we have fount the perfect insertion so we store it in the replace index variable
                replaceIndex = i;

                // Se the flag that we found a perfect insertion point
                found = true;

                // Then we break out of the loop since we have already found the optimal insertion point
                break;               
            }
        }

        // If found is false then the order book is already sorted (out of luck), if not we need to rearrange PARTS of the array        
        if (found){
            // Put the newly added order in a temporary variable
            Order memory temp = orders[orders.length-1];

            // Then loop backwards in the array until we reach the insertion point
            for(uint i = orders.length -1; i > replaceIndex; i--){
                // Move each order one step to the "right"
                orders[i] = orders[i-1];
            }

            // Insert the order in the perfect location
            orders[replaceIndex] = temp;
        }
    }

    /* Creates a market order and makes the relevant swaps in the order book
        Ticker: The ticker that the order should be associated with
        Side: if the order should be a buy or sell order
        Amount: How many units of the ticker that the user wants to sell or buy
     */
    function createMarketOrder(bytes32 ticker, Side side, uint amount) tokenExist(ticker) public {

        // If it is a sell order, make sure that the user have enough funds to the sell
        if (side == Side.SELL){
            require(balances[msg.sender][ticker] >= amount,"Not enough funds");            
        }

        // Reference to the relevant order book
        Order[] storage orders = orderBook[ticker][uint(side == Side.BUY ? Side.SELL : Side.BUY)];

        // Return if book is empty
        if (orders.length == 0) return;

        // Define a variable for the remaining amount to fill
        uint remaining = amount;

        // The price for the amount purchased on a given order 
        uint price;

        // The amount to fill on a given order
        uint toFill;

        // The seller of the order
        address buyer;

        // The buyer of the order
        address seller;

        // Loop though the order book and stop if we reached the end of the order book or the order has been filled
        for(uint i = 0 ; i < orders.length && remaining > 0; i++){
            
            // Skip trades where the user is the trader
            if (orders[i].trader == msg.sender) continue;

            // If the remaining amount is greater or equal to the available amount in the order
            if (remaining >= orders[i].amount.sub(orders[i].filled)){

                // Then set the amount to fill on the order based on the remaining amound on the order
                toFill = orders[i].amount.sub(orders[i].filled);

                // Set the price of the order
                price = orders[i].amount.mul(orders[i].price);           
            }
            else
            {
                // If we get here then the current order has enough amount to fill the rest of the order

                // Set the price of the order
                price = remaining.mul(orders[i].price);

                // Set the amount to fill to the remaining nessecary amount to fill the order
                toFill = remaining;
            }

            // Subtract the remaining amount based on the amount to fill on the order
            remaining = remaining.sub(toFill);   

            // If it is a buy order
            if (side == Side.BUY){

                // Then the seller is the caller
                buyer = msg.sender;

                // And the seller is the creator of the order
                seller = orders[i].trader;

                // Make sure that the buyer (caller) has enough ETH to complete the order
                require(balances[buyer]["ETH"] >= price, "Not enough ETH");

                // Also make sure that the seller has enough tokens left in his account
                require(balances[seller][ticker] >= toFill, "Seller has not enough funds");                              
            }else{
                // If we get here then it is a sell order

                // And therefore the buyer is the creator of the order
                buyer = orders[i].trader;

                // And the seller is the caller
                seller = msg.sender;

                // Make sure that the buyer have enough funds to complete the purchase
                if (balances[buyer]["ETH"] < price) continue;
            }

            // Since we now know who the seller and buyer is, lets complete the order

            // Transfer the ETH from the buyer to the seller
            balances[buyer]["ETH"] = balances[buyer]["ETH"].sub(price);
            balances[seller]["ETH"] = balances[seller]["ETH"].add(price);

            // Transfer the Tokens from the seller to the buyer
            balances[seller][ticker] = balances[seller][ticker].sub(toFill);
            balances[buyer][ticker] = balances[buyer][ticker].add(toFill);  

            // Set hte filled amount of the order
            orders[i].filled = orders[i].filled.add(toFill);

            // Make sure that we have not over filled the order
            assert(orders[i].filled <= orders[i].amount);
        }

        // Remove all the totally filled orders from the order book
        removeFilledOrders(ticker, side == Side.BUY ? Side.SELL : Side.BUY);
    }


    /* Removed order from the order book that is 100% filled
     */
    function removeFilledOrders(bytes32 ticker, Side side) private {

        // Get the orders for the ticker in the given order book
        Order[] storage orders = orderBook[ticker][uint(side)];

        // Return if the book is already empty
        if (orders.length == 0) return;

        // If the first order is not filled then we assume that the book is already filtered
        if (orders[0].amount.sub(orders[0].filled) > 0) return;

        // Define a variable to hold the index of the first unfilled order in the book
        uint firstUnfilledIndex;

        // Loop through the orders
        for(uint i = 0; i < orders.length; i++){
            
            // Continue if the order has been filled
            if (orders[i].amount.sub(orders[i].filled) == 0) continue;

            // If we get here then set the variable to this index and break since we found the index we are looking for
            firstUnfilledIndex = i;
            break;
        }

        // If we found an unfilled order
        if (firstUnfilledIndex > 0){

            // Start looping from the index of the first unfilled order and loop through the order book
            for(uint i = 0; i < firstUnfilledIndex; i++){
                // Break if the last item is the first unfulfilled so we dont go outside the array
                if (i+ firstUnfilledIndex>= orders.length) break;

                // Move the order backwards with an offset of the first unfillet order (the first unfilled order should now be on index 0)
                orders[i] = orders[i + firstUnfilledIndex];
            }
        }

        // If we found unfilled orders then we need to remove the number of filled orders
        // If we did not find any unfilled orders that means that all orders are filled so we set the variable to the length of the order book
        uint ordersToRemove = firstUnfilledIndex > 0? firstUnfilledIndex :orders.length; 
        require(ordersToRemove <= orders.length, "To many orders to remove");
        // Remove the filled orders 
        for(uint i = 0; i < ordersToRemove; i++){
            orders.pop();
        }
    }

    /* 
        Deposits eth into the wallet of the caller
     */
    function depositETH() payable public {
        balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].add(msg.value); 
    }
}

Unit tests

DEX unit 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 book should be ordered on price from lowest to highest starting at index 0
// The User should not be able to create for not supported tokens



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

contract.skip("Dex", accounts => {

    let dex;
    let link;
    let eth;

    before(async function(){
        dex = await Dex.deployed();
        link = await Link.deployed();
        eth = await Eth.deployed();
        await dex.addtoken(web3.utils.fromUtf8("ETH"), eth.address, {from: accounts[0]})
        await dex.addtoken(web3.utils.fromUtf8("LINK"), link.address, {from: accounts[0]})
    });


    it("should only be possible to create a BUY limit order that the user can afford", async () => {

        await truffleAssert.reverts(
            dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 0, 3,1)
        );

        await dex.depositETH({value: 30});

        await truffleAssert.passes(
            dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 0, 3,1)
        );
    })

    it("should only be possible to create a SELL limit order that the user can afford", async () => {
        
        await truffleAssert.reverts(
            dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 1, 5,1)
        );

        await link.approve(dex.address, 5)
      
        await dex.deposit(5, web3.utils.fromUtf8("LINK"));

        await truffleAssert.passes(
            dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 1, 5,1)
        );
    })
    it("should order the BUY order book in decending order", async () => {
        const invalidOrder = [12,2,5,9,9,45,3];
        await link.approve(dex.address, 10000);
        await dex.depositETH({value: 100000});
        await dex.deposit(10000, web3.utils.fromUtf8("LINK"));

        for (let i = 0; i< invalidOrder.length; i++){
            await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 0, invalidOrder[i] * 2, invalidOrder[i]);
        }

        const orderBook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 0);
      
        assert(orderBook.length > 0, "Missing orders in the order book");
        for (let i = 0; i< orderBook.length -1; i++){        
            assert(parseInt(orderBook[i].price) >= parseInt(orderBook[i+1].price), "Invalid BUY price order")
        }
    })

    it("should order the SELL order book in ascending order", async () => {
        const invalidOrder = [12,2,5,9,9,45,3];
        await link.approve(dex.address, 1000);

        for (let i = 0; i< invalidOrder.length; i++){
            await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 1, invalidOrder[i] * 2, invalidOrder[i]);
        }

        const orderBook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1);
     
        assert(orderBook.length > 0, "Missing orders in the order book");
        for (let i = 0; i< orderBook.length -1; i++){
            assert(parseInt(orderBook[i].price) <= parseInt(orderBook[i+1].price), "Invalid SELL price order")
        }
    })

    it("should only be possible for the user to create orders for supported tokens", async () => {
        await link.approve(dex.address, 1000);
        await dex.deposit(600, web3.utils.fromUtf8("LINK"));
        await truffleAssert.passes(dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 0, 500, 1));
        await truffleAssert.passes(dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 1, 500, 2));
        await truffleAssert.reverts(dex.createLimitOrder(web3.utils.fromUtf8("ADA"), 0, 500, 1));
        await truffleAssert.reverts(dex.createLimitOrder(web3.utils.fromUtf8("ADA"), 1, 500, 2));
    })
})

contract("Dex", accounts => {

    let dex;
    let link;
    let eth;

    beforeEach(async function(){
        dex = await Dex.new();
        link = await Link.new();
        eth = await Eth.new();
        await dex.addtoken(web3.utils.fromUtf8("ETH"), eth.address, {from: accounts[0]})
        await dex.addtoken(web3.utils.fromUtf8("LINK"), link.address, {from: accounts[0]})

        await link.transfer(accounts[1], 1000000);
        await eth.transfer(accounts[1], 1000000);
    });

    // When creating SELL market order, the seller needs to have enough tokens for the trade
    // When creating a BUY market order, the buyer needs to have enough ETH for the trade
    // Market orders can be submitted even if the order book is empty
    // Market orders should be filled until the order book is empty or the market order is 100% filled
    // The ETH balance of the buyer should decrease with the filled amounts.
    // The token balances of the limit order sellers should decrese with the filled amounts.
    // Filled limit orders should be removed from the orderbook

    it("should only be possible to create a BUY market order that the user can afford", async () => {

        await dex.depositETH({value: 30})
        await dex.depositETH({value: 30, from: accounts[1]});
        await link.approve(dex.address, 100)    
        await link.approve(dex.address, 100, {from: accounts[1]})    
        await dex.deposit(100, web3.utils.fromUtf8("LINK"),);
        await dex.deposit(100, web3.utils.fromUtf8("LINK"), {from: accounts[1]});

        await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 1, 50, 3, {from: accounts[1]});
        

        await truffleAssert.reverts(
            dex.createMarketOrder(web3.utils.fromUtf8("LINK"), 0, 50)
        );
        await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 1, 50, 1, {from: accounts[1]});
        await truffleAssert.passes(
            dex.createMarketOrder(web3.utils.fromUtf8("LINK"), 0, 30)
        );
    })

    it("should only be possible to create a SELL market order that the user can afford", async () => {
        
        await link.approve(dex.address, 100)  
        await link.approve(dex.address, 100, {from: accounts[1]})    
        await dex.deposit(100, web3.utils.fromUtf8("LINK"));
        await dex.deposit(100, web3.utils.fromUtf8("LINK"), {from: accounts[1]});
        await dex.depositETH({value: 300});
        await dex.depositETH({value: 300, from: accounts[1]});
        await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 0, 200, 1, {from: accounts[1]});

        await truffleAssert.reverts(
            dex.createMarketOrder(web3.utils.fromUtf8("LINK"), 1, 200)
        );

        await truffleAssert.passes(
            dex.createMarketOrder(web3.utils.fromUtf8("LINK"), 1, 100)
        );
    })

    it("should be possible to submit a market order even if the orderbook is empty", async () => {
        await link.approve(dex.address, 100)  
        await link.approve(dex.address, 100, {from: accounts[1]})      
        await dex.deposit(100, web3.utils.fromUtf8("LINK"));
        await dex.deposit(100, web3.utils.fromUtf8("LINK"), {from: accounts[1]});
        await dex.depositETH({value: 300});
        await dex.depositETH({value: 30, from: accounts[1]});
        await truffleAssert.reverts(
            dex.createMarketOrder(web3.utils.fromUtf8("LINK"), 1, 200)
        );

        await truffleAssert.passes(
            dex.createMarketOrder(web3.utils.fromUtf8("LINK"), 1, 100)
        );
    })

    it("should fill market orders until the order book is empty or order is filled", async () => {
        await link.approve(dex.address, 1000);   
        await link.approve(dex.address, 1000, {from: accounts[1]});    
        await dex.deposit(1000, web3.utils.fromUtf8("LINK"));
        await dex.deposit(1000, web3.utils.fromUtf8("LINK"), {from: accounts[1]});
        await dex.depositETH({value: 5000});
        await dex.depositETH({value: 5000, from: accounts[1]});

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

        await dex.createMarketOrder(web3.utils.fromUtf8("LINK"), 0, 120);
        
        let orderBook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1);
  
        assert.equal(orderBook.length, 2);

        await dex.createMarketOrder(web3.utils.fromUtf8("LINK"), 0, 50);
        orderBook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1);
        assert.equal(orderBook.length, 1);

        await dex.createMarketOrder(web3.utils.fromUtf8("LINK"), 0, 30);
        orderBook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1);

        assert.equal(orderBook.length, 0);
    })

    it("should decrease the ETH amount on buy orders of the buyer for every filled amount", async () => {
        await link.approve(dex.address, 1000);    
        await link.approve(dex.address, 1000, {from: accounts[1]});    
        await dex.deposit(1000, web3.utils.fromUtf8("LINK"));
        await dex.deposit(1000, web3.utils.fromUtf8("LINK"), {from: accounts[1]});
        await dex.depositETH({value: 5000});
        await dex.depositETH({value: 5000, from: accounts[1]});
        await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 1, 50, 1, {from: accounts[1]});
        await dex.createMarketOrder(web3.utils.fromUtf8("LINK"), 0, 50);
        let balance = await dex.balances(accounts[0], web3.utils.fromUtf8("ETH"));
        assert.equal(balance.toNumber(), 4950);
    })

    it("should decrease the token amount on sell orders of the buyer for every filled amount", async () => {
        await link.approve(dex.address, 1000);   
        await link.approve(dex.address, 1000, {from: accounts[1]});     
        await dex.deposit(1000, web3.utils.fromUtf8("LINK"));
        await dex.deposit(1000, web3.utils.fromUtf8("LINK"), {from: accounts[1]});
        await dex.depositETH({value: 5000});
        await dex.depositETH({value: 5000, from: accounts[1]});
        await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 0, 50, 1, {from: accounts[1]});
        await dex.createMarketOrder(web3.utils.fromUtf8("LINK"), 1, 50);
        let balance = await dex.balances(accounts[0], web3.utils.fromUtf8("LINK"));

        assert.equal(balance.toNumber(), 950);
    })

    it("should remove filled orders from the order book", async () => {
        await link.approve(dex.address, 1000);    
        await link.approve(dex.address, 1000, {from: accounts[1]}); 
        await dex.depositETH({value: 5000});
        await dex.depositETH({value: 5000, from: accounts[1]});
        await dex.deposit(1000, web3.utils.fromUtf8("LINK"));
        await dex.deposit(1000, web3.utils.fromUtf8("LINK"), {from: accounts[1]});

        await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 1, 50, 3, {from: accounts[1]});       
        await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 1, 50, 2, {from: accounts[1]});        
        await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 1, 50, 2, {from: accounts[1]});       
        await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 1, 50, 1, {from: accounts[1]});
        await dex.createMarketOrder(web3.utils.fromUtf8("LINK"), 0, 180);
        
        let orderBook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1);


        assert.equal(orderBook.length, 1);

    })
})

Perhaps I will do a front end for it in the future, but for now I will take the next course…

3 Likes

Here is my DEX Repo

Will try to build a front-end as soon as I am finished with my other project.

Thank you @filip for taking us through this. This journey was a true blast! I did learn a lot and boy is there a lot to learn :sweat_smile:

Let’s keep on pushing! :sunglasses: (not only arrays, but also our skills…)

5 Likes

Ok, so I took on the challenge to create a frontend for the DEX. So far so good, I was able to implement the following functions successfully:
Deposit, Withdraw, Create Limit Buy Order, Create Limit Sell Order
The Buy Order and Sell Order Book will be printed and updated as needed.

But now I am stuck with the addToken function. It will always revert. I tried to execute it in the migrations and via the frontend, but no success. It will always revert, although I call it from the owner address. You can find my repo here.
If anyone finds time to take a look into it, I will be very happy :slight_smile: @filip @Capplequoppe @dan-i @thecil addTokenRevert

4 Likes

Hey @Bhujanga, hope you are ok.

This is your function:

  function addToken(bytes32 ticker, address tokenAddress) onlyOwner external{ //to add a new token, called from another contract therefore external
    tokenMapping[ticker] = Token(ticker, tokenAddress);             //creates new token struct and adds it to the token mapping using the ticker as key.struct holds ticker and address as parameters
    tokenList.push(ticker);                                         //pushes ticker to tokenList
  }

What i dont understand is why you are sending a value in eth if the functions does not need to receive any funds, also you are using fromAsiic, which could return the value of “LINK” in a format that the functioin does not recognize , what about trying it with fromUtf8 like you did in your unit tests?

Carlos Z

1 Like

Hi Carlos,
thanks for replying. Yes you are right, I also found out by now that I do not have to send any value to this function, although I wonder why not? Isn’t this function changing the storage of the contract and therefore makes changes to the blockchain (and therefore requires gas?) ? :thinking:

I got it running right now and can add Tokens :partying_face:

I use from Ascii as the function receives the ticker as bytes32. It works just the same as when using Utf8, just tested it now. I used Ascii due to research I did earlier. Would you recommend using Utf8 as Ascii could lead to errors?

Thanks for looking into this! I will keep you updated :slight_smile:

Ok, so I am now quite puzzled on how to actually mint tokens from the token contract. Please have a look at my project to understand the contracts and the inheritance structure. I try to describe my problem:

I migrate Dex.sol and Link.sol
Dex.sol inherits from Wallet.sol which inherits from IERC20.sol
Link.sol inherits from ERC20.sol

I add tokens to the tokenMapping with
await dex.methods.addToken(web3.utils.fromUtf8(newTicker), "0x2F7e1754aB78B574cC1fAfb4c93556fa6e90F194").send();
Address is referring to Link.sol (Token Contract)

But how do I actually MINT TOKENS?
In the Link.sol (TokenContract) Constructor, it is supposed to mint 1000 to msg.sender.
LinkSolMint
But when printing balances for all tokens of ethereum.selectedAddress

  async function prtTokenBalances(){
      let tokenListLength = await dex.methods.getTokenListLength().call();
      for (let i = 0; i < tokenListLength; i++){
        let tokenListVar = await dex.methods.tokenList(i).call();
        let newBalance = await dex.methods.balances(ethereum.selectedAddress, tokenListVar).call();
        console.log("Balance of " + web3.utils.toUtf8(tokenListVar) + " " + newBalance);
      }
    }

Balance will be still 0. ???
LinkBalance0

Dude, I am confused :joy:
Where do I go wrong? What piece of the puzzle am I missing? I tried to instantiate the Link.sol inside the main.js, but with no luck due to this Error, although I did provide the abi file for Link.sol

You must provide the json interface of the contract when instantiating a contract object.

I then tried to import Link.sol into Dex.sol, but this somehow got me even more confused and yea, to make long story short, can you help me get this important step of this DEX working? All functionality is given, but only the minting of the Tokens themselves, I totally am lost. lol.
In the unit tests, I can call

  await link.approve(dex.address, 500);
        await dex.deposit(10, web3.utils.fromUtf8("LINK"));

But i do not have a link instance in my dex.sol
and dex.deposit throws error is not a function

:face_with_monocle: :thinking: :exploding_head:

I feel like I am so close to get this project right. I hope I did not confuse you too much. :sweat_smile: :pray:

1 Like

Here is my final project, I’m facing a weird invalid opcode from the test, but testing it manually on truffle develop the issue does not appear, researching a little bit apparently is a problem with truffle, so I will try to deploy the contracts but with Hardhat. Maybe I’m just missing something :face_with_monocle:

https://github.com/thecil/Project-Dex

Carlos Z

2 Likes

I think you are getting confuse with the balance of different contracts, you first deploy the Link contract, which will mint 1000 to msg.sender, that balance is on the link contract, not on the Dex contract.

After you mint the links, you must approve the Dex contract to spend your links, then you can deposit on the Dex and the balance of link will be show in the contract.

Carlos Z

1 Like

Ok, so far so good. I could write a frontend for all functionality given from the DEX that @filip coded with us in the 201 course. Watch this video where I walk you through it. One of the things left to do is to make the DApp also fill Limit Orders. It seems that we did not implement this functionality in the course. I will go through the whole course again as soon as I find the time. If it is so, then I will try to implement it myself. But for now, I am happy with the achievement of building the frontend for what is there :slight_smile: Can’t wait to see more courses on DApp development here at the academy. I am hooked already on building DApps and am very happy that I start to understand things and can see clearer with every problem that I am able to solve. Thanks again to @filip for teaching us and many thanks to @thecil, @dan-i, and @Malik for their support and answering my questions :pray: Without your help, this journey would be much, much harder.

old

Alright, the deposit function works! REPO is up to date
Now the only thing that is not working properly is, that the Limit orders are not filled, even if there are two exact orders in the orderbook. :thinking:

OrdersNotFilled

older

Thank you Carlos, you were right! After taking a break I did figure it out. So the LINK balance is now displayed correctly. The thing I am struggling with right now is, that I can now create both, Sell and Buy orders, but they are not filled, but just sit there. Why?
allowance-orderFills

Also, I can withdraw Tokens, but I cannot deposit them due to “exceeds allowance” although I approve the dex in the migrations with enough. Do you see where I go wrong?

I highly appreciate your help mate! :pray:
Happy Sunday!

4 Likes

Hey @thecil

I dm you let me know :slight_smile:

1 Like

oof finally, this last one was the hardest homework for me o, im really bad at loops, so this was a good learning experience, also i realize how much theres left to learn, i finished my DEX while doing the tests homework, so yes i havent watched Filips recommendations on how to do this market ordersso dont kill me yet D: , i will post an updated version of my DEX once i watch Filips videos, one thing i notice right of, is that my management of traders balances looks risky *o *, and that there is a lot of duplicated code, and about the tests, theres only 4 tests, but ther last two tests actually check two things at once, and i created a ERC20 called ETH, but it seems that ETH s alread like built in xD, so yes sorry about that xp

MarketOrderTest.js
const Dex = artifacts.require("Dex")
const Ethereum = artifacts.require("Ethereum")
const Link = artifacts.require("Link")
const { reverts } = require('truffle-assertions');
const truffleAssert = require('truffle-assertions');

contract("Dex", accounts => { 
    it.skip("should only be possible to buy if you have enough ETH", async () => {
        let dex = await Dex.deployed()
        let eth = await Ethereum.deployed()
        await dex.addToken(web3.utils.fromUtf8("ETH"), eth.address, {from: accounts[0]})
        await eth.approve(dex.address, 500);
        await dex.deposit(100, web3.utils.fromUtf8("ETH"));
        await truffleAssert.reverts(
            dex.createMarketOrder(true, web3.utils.fromUtf8("LINK"), 200)
        ) 

    })

    it.skip("should have enough tokens such that tokens balance >= sell order amount ", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()
        await dex.addToken(web3.utils.fromUtf8("LINK"), link.address, {from: accounts[0]})
        await link.approve(dex.address, 500);
        await dex.deposit(100, web3.utils.fromUtf8("LINK"));
        await truffleAssert.reverts(
            dex.createMarketOrder(false, web3.utils.fromUtf8("LINK"), 150)
        ) 

    })

    it.skip("Orders can be submitted even when orderbook is empty", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()
        let eth = await Ethereum.deployed()
        await dex.addToken(web3.utils.fromUtf8("LINK"), link.address, {from: accounts[0]})
        await link.approve(dex.address, 500);
        await dex.deposit(100, web3.utils.fromUtf8("LINK"));
        await dex.createMarketOrder(false, web3.utils.fromUtf8("LINK"),6)

        let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), false);
        assert(orderbook[0].amount == 6, "it should1"); 

    })

    it.skip("Orders should be filled until orderbook is empty or until it is filled 100%", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()
        let eth = await Ethereum.deployed()
        await dex.addToken(web3.utils.fromUtf8("LINK"), link.address, {from: accounts[0]})
        await dex.addToken(web3.utils.fromUtf8("ETH"), eth.address, {from: accounts[0]})
        await link.approve(dex.address, 500);
        await eth.approve(dex.address, 500);
        await dex.deposit(100, web3.utils.fromUtf8("LINK"));
        await dex.deposit(100, web3.utils.fromUtf8("ETH"));
        await dex.createLimitOrder(false, web3.utils.fromUtf8("LINK"),6, 1)
        await dex.createLimitOrder(false, web3.utils.fromUtf8("LINK"),3, 1)
        await dex.createMarketOrder(true, web3.utils.fromUtf8("LINK"),20)

        let orderbookBuyers = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), true);
        let orderbookSellers = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), false);
        assert(orderbookBuyers[0].amount == 11, "you wanted 20 and there was only 9 for sale");
        assert(orderbookSellers.length == 0, "whale bought");        

    })

    it("Balances of ETH and LINK should be updated", async () => {
        let dex = await Dex.deployed() 
        let link = await Link.deployed()
        let eth = await Ethereum.deployed()
        await dex.addToken(web3.utils.fromUtf8("LINK"), link.address, {from: accounts[0]})
        await dex.addToken(web3.utils.fromUtf8("ETH"), eth.address, {from: accounts[0]})
        await link.transfer(accounts[1], 50)
        await link.approve(dex.address, 50, {from: accounts[1]});
        await eth.approve(dex.address, 500);    
        await dex.deposit(50, web3.utils.fromUtf8("LINK"), {from: accounts[1]});
        await dex.deposit(100, web3.utils.fromUtf8("ETH"));

        await dex.createLimitOrder(false, web3.utils.fromUtf8("LINK"), 6, 2, {from: accounts[1]})
        await dex.createMarketOrder(true, web3.utils.fromUtf8("LINK"),6)

        let balanceEth0 = await dex.balances(accounts[0],web3.utils.fromUtf8("ETH"));
        let balanceLink0 = await dex.balances(accounts[0],web3.utils.fromUtf8("LINK"));
        let balanceEth1 = await dex.balances(accounts[1],web3.utils.fromUtf8("ETH"));
        let balanceLink1 = await dex.balances(accounts[1],web3.utils.fromUtf8("LINK"));

        assert(balanceEth0 == 94, "cause you spent 6 eth");  
        assert(balanceLink0 == 3, "cause each link was 2 eth");  
        assert(balanceEth1 == 6, "they bought 3 link for 2 eth each");   
        assert(balanceLink1 == 47, "cause you sold 3 link");  
    })

})
DEX.sol
pragma solidity ^0.8.0;

pragma abicoder v2;

import "../contracts/wallet.sol";

contract Dex is Wallet{

    using SafeMath for uint256; 


struct Order{
    uint id;
    address trader;
    bool buySell;
    bytes32 ticker;
    uint amount;
    uint price;
}

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

uint public nextOrderId;

uint marketPrice = 1;


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



function createMarketOrder(bool buys, bytes32 ticker, uint amount)public{

    if(buys == true){
        require(amount <= balances[msg.sender]["ETH"], "not enough funds");
        while(amount > 0 ){
            
            if(orderBook[ticker][false].length == 0 || orderBook[ticker][false][0].price > amount){
                createLimitOrder (buys, ticker, amount, marketPrice);
                break;
            }

                amount -= orderBook[ticker][false][0].price;
                orderBook[ticker][false][0].amount -= 1;
                balances[msg.sender][ticker] += 1;
                balances[msg.sender]["ETH"] -= orderBook[ticker][false][0].price;
                balances[orderBook[ticker][false][0].trader][ticker] -= 1;
                balances[orderBook[ticker][false][0].trader]["ETH"] += orderBook[ticker][false][0].price;
                marketPrice = orderBook[ticker][false][0].price;
                 
                if(orderBook[ticker][false][0].amount == 0){
                    orderBook[ticker][false][0].price += orderBook[ticker][false][orderBook[ticker][false].length-1].price;
                    for(uint i = 0; i < orderBook[ticker][false].length-1; i++ ){
                       
                        if(orderBook[ticker][false][i].price > orderBook[ticker][false][i+1].price){
                                 Order memory swap = orderBook[ticker][false][i];
                                 orderBook[ticker][false][i] = orderBook[ticker][false][i+1];
                                 orderBook[ticker][false][i+1] = swap;
                        }
           
                    }  
                    orderBook[ticker][false].pop();
                }
            
            }
        
        }     

    if(buys == false){
        require(amount <= balances[msg.sender][ticker], "not enough funds");
        while(amount > 0 ){
            
            if(orderBook[ticker][true].length == 0){
                createLimitOrder (buys, ticker, amount, marketPrice);
                break;
            }
        
            else if(orderBook[ticker][true].length > 0){
                amount -= 1;
                orderBook[ticker][true][0].amount -= 1;
                balances[msg.sender][ticker] -= 1;
                balances[msg.sender]["ETH"] += orderBook[ticker][true][0].price;
                balances[orderBook[ticker][true][0].trader][ticker] += 1;
                balances[orderBook[ticker][true][0].trader]["ETH"] -= orderBook[ticker][true][0].price;
                marketPrice = orderBook[ticker][true][0].price;
                 
                if(orderBook[ticker][true][0].amount == 0){
                    orderBook[ticker][true][0].price = 0;
                    for(uint i = 0; i < orderBook[ticker][true].length-1; i++ ){
                       
                        if(orderBook[ticker][true][i].price < orderBook[ticker][true][i+1].price){
                             Order memory swap = orderBook[ticker][true][i];
                             orderBook[ticker][true][i] = orderBook[ticker][true][i+1];
                             orderBook[ticker][true][i+1] = swap;
                        }
           
                    }  
                     orderBook[ticker][true].pop();
                }
            }
        }
        
    }     
    nextOrderId++;
}



 function createLimitOrder (bool buys, bytes32 ticker, uint amount, uint price)public {
    if(buys == true){
        require(amount*price <= balances[msg.sender]["ETH"], "not enough funds");
        
            orderBook[ticker][buys].push(Order(nextOrderId, msg.sender, buys, ticker, amount, price));
            for(uint i = orderBook[ticker][buys].length-1; i > 0; i--){
            if(orderBook[ticker][buys][i].price > orderBook[ticker][buys][i-1].price){
               uint swap = orderBook[ticker][buys][i].price;
               orderBook[ticker][buys][i].price = orderBook[ticker][buys][i-1].price;
               orderBook[ticker][buys][i-1].price = swap;
            }
        }

    }    

     else if(buys == false){
         require(amount <= balances[msg.sender][ticker], "not enough funds");
         
         orderBook[ticker][buys].push(Order(nextOrderId, msg.sender, buys, ticker, amount, price));
            for(uint i = orderBook[ticker][buys].length-1; i > 0; i--){
            if(orderBook[ticker][buys][i].price < orderBook[ticker][buys][i-1].price){
               uint swap = orderBook[ticker][buys][i].price;
               orderBook[ticker][buys][i].price = orderBook[ticker][buys][i-1].price;
               orderBook[ticker][buys][i-1].price = swap;
            }
        }    
    
    }

    nextOrderId++;
}

}


Yes my marketorder lacks sophistication and its expensive to run, one trick that i specially liked, was to move the entire book to the left, instead of switching the filled orders as i was doing, so im definetely copying that, and i gotta go watch the rest of the video xD

DEX SSJ1
pragma solidity ^0.8.0;

pragma abicoder v2;

import "../contracts/wallet.sol";

contract Dex is Wallet{

    using SafeMath for uint256; 


struct Order{
    uint id;
    address trader;
    bool buySell;
    bytes32 ticker;
    uint amount;
    uint price;
}

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

uint public nextOrderId;

uint marketPrice = 1;


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



function createMarketOrder(bool buys, bytes32 ticker, uint amount)public{

    if(buys == true){
        require(amount <= balances[msg.sender]["ETH"], "not enough funds");
        while(amount > 0 ){
            
            if(orderBook[ticker][false].length == 0 || orderBook[ticker][false][0].price > amount){
                createLimitOrder (buys, ticker, amount, marketPrice);
                break;
            }

                amount -= orderBook[ticker][false][0].price;
                orderBook[ticker][false][0].amount -= 1;
                balances[msg.sender][ticker] += 1;
                balances[msg.sender]["ETH"] -= orderBook[ticker][false][0].price;
                balances[orderBook[ticker][false][0].trader][ticker] -= 1;
                balances[orderBook[ticker][false][0].trader]["ETH"] += orderBook[ticker][false][0].price;
                marketPrice = orderBook[ticker][false][0].price;
                 
                if(orderBook[ticker][false][0].amount == 0){
                    uint i = 0;
                    while(i < orderBook[ticker][false].length-1){
                        orderBook[ticker][false][i] = orderBook[ticker][false][i+1];
                        i++;
                    }
                    orderBook[ticker][false].pop();
                }
            
            }
        
        }     

    if(buys == false){
        require(amount <= balances[msg.sender][ticker], "not enough funds");
        while(amount > 0 ){
            
            if(orderBook[ticker][true].length == 0){
                createLimitOrder (buys, ticker, amount, marketPrice);
                break;
            }
                amount -= 1;
                orderBook[ticker][true][0].amount -= 1;
                balances[msg.sender][ticker] -= 1;
                balances[msg.sender]["ETH"] += orderBook[ticker][true][0].price;
                balances[orderBook[ticker][true][0].trader][ticker] += 1;
                balances[orderBook[ticker][true][0].trader]["ETH"] -= orderBook[ticker][true][0].price;
                marketPrice = orderBook[ticker][true][0].price;
                 
                if(orderBook[ticker][true][0].amount == 0){
                    uint i = 0;
                while(i < orderBook[ticker][true].length-1){
                    orderBook[ticker][true][i] = orderBook[ticker][true][i+1];
                    i++;
                }
                orderBook[ticker][true].pop();
            }
        }
        
    }     
    nextOrderId++;
}



 function createLimitOrder (bool buys, bytes32 ticker, uint amount, uint price)public {
    if(buys == true){
        require(amount*price <= balances[msg.sender]["ETH"], "not enough funds");
        
            orderBook[ticker][buys].push(Order(nextOrderId, msg.sender, buys, ticker, amount, price));
            for(uint i = orderBook[ticker][buys].length-1; i > 0; i--){
            if(orderBook[ticker][buys][i].price > orderBook[ticker][buys][i-1].price){
               uint swap = orderBook[ticker][buys][i].price;
               orderBook[ticker][buys][i].price = orderBook[ticker][buys][i-1].price;
               orderBook[ticker][buys][i-1].price = swap;
            }
        }

    }    

     else if(buys == false){
         require(amount <= balances[msg.sender][ticker], "not enough funds");
         
         orderBook[ticker][buys].push(Order(nextOrderId, msg.sender, buys, ticker, amount, price));
            for(uint i = orderBook[ticker][buys].length-1; i > 0; i--){
            if(orderBook[ticker][buys][i].price < orderBook[ticker][buys][i-1].price){
               uint swap = orderBook[ticker][buys][i].price;
               orderBook[ticker][buys][i].price = orderBook[ticker][buys][i-1].price;
               orderBook[ticker][buys][i-1].price = swap;
            }
        }    
    
    }

    nextOrderId++;
}

}


my dex after i copied filips trick, theres more to fix i know, will post when i have an almost finished version

2 Likes

Hey there, here’s my code:

Dex.sol
pragma solidity >=0.6.0 <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;
        bool 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"] >= price.mul(amount));
        }

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

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

/*

        if(side == Side.BUY){
            uint posToCheck = orders.length > 0 ? orders.length - 1 : 0;
            for(uint i = 0; i < orders.length; i++){
                if(orders[posToCheck].price > orders[posToCheck-1].price){
                    Order memory temp = orders[posToCheck-1];
                    orders[posToCheck-1] = orders[posToCheck];
                    orders[posToCheck] = temp;
                }
            posToCheck--;
            }
        }
        else if(side == Side.SELL){            
            uint posToCheck = orders.length > 0 ? orders.length - 1 : 0;
            for(uint i = 0; i < orders.length; i++){
                if(orders[posToCheck].price < orders[posToCheck-1].price){
                    Order memory temp = orders[posToCheck-1];
                    orders[posToCheck-1] = orders[posToCheck];
                    orders[posToCheck] = temp;
                }
            }
            posToCheck--;
        }
*/
        nextOrderId++;
        
    }

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

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

        for(uint256 i = 0; i < orders.length && totalFilled < amount; i++){
            uint limitOrdAvail = orders[i].amount;
            uint payAmount;
            if(toFill < limitOrdAvail){
                if (orderBookSide == 1){
                    payAmount = (orders[i].price)*toFill;
                    require(balances[msg.sender]['ETH'] >= payAmount, "token buyer has insuficient ETH balance");
                    totalFilled += toFill;
                    orders[i].amount -= toFill;
                    balances[orders[i].trader][ticker] -= toFill;
                    balances[msg.sender][ticker] += toFill;
                    balances[orders[i].trader]["ETH"] += payAmount;
                    balances[msg.sender]["ETH"] -= payAmount;
                }
                else if(orderBookSide == 0){
                    payAmount = (orders[i].price)*toFill;
                    require(balances[orders[i].trader]['ETH'] >= payAmount, "token seller has insuficient ETH balance");
                    totalFilled += toFill;
                    orders[i].amount -= toFill;
                    balances[orders[i].trader][ticker] += toFill;
                    balances[msg.sender][ticker] -= toFill;
                    balances[orders[i].trader]["ETH"] -= payAmount;
                    balances[msg.sender]["ETH"] += payAmount;
                }
            }
            else{
                if (orderBookSide == 1){
                    payAmount = (orders[i].price)*limitOrdAvail;
                    require(balances[msg.sender]['ETH'] >= payAmount, "token buyer has insuficient ETH balance");
                    totalFilled += limitOrdAvail;
                    toFill = amount - totalFilled;
                    orders[i].amount -= limitOrdAvail;
                    balances[orders[i].trader][ticker] -= limitOrdAvail;
                    balances[msg.sender][ticker] += limitOrdAvail;
                    balances[orders[i].trader]["ETH"] += payAmount;
                    balances[msg.sender]["ETH"] -= payAmount;
                }
                else if(orderBookSide == 0){
                    payAmount = (orders[i].price)*limitOrdAvail;
                    require(balances[orders[i].trader]['ETH'] >= payAmount, "token seller has insuficient ETH balance");
                    totalFilled += limitOrdAvail;
                    toFill = amount - totalFilled;
                    orders[i].amount -= limitOrdAvail;
                    balances[orders[i].trader][ticker] += limitOrdAvail;
                    balances[msg.sender][ticker] -= limitOrdAvail;
                    balances[orders[i].trader]["ETH"] -= payAmount;
                    balances[msg.sender]["ETH"] += payAmount;
                }
                orders[i].filled = 1;
            }
        }
        if(order.length>0){
            for(uint256 i = 0; i < orders.length;){
                if (orders[i].filled == 1){
                    for(uint256 j = i; j < orders.length - 1; j++){
                        orders[j] = orders[j+1];
                    }
                    orders.pop();
                }
                else{
                    break;
                }
            }
        }
    }
}
Tokens.sol
pragma solidity >=0.6.0 <0.8.0;

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

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

contract ETH is ERC20 {
    constructor() ERC20("Ether", "ETH") public {
        _mint(msg.sender, 100000);
    }
}
Wallet.sol
pragma solidity >=0.6.0 <0.8.0;

import "../node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../node_modules/@openzeppelin/contracts/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 exist");
        _;
    }

    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 enough");
        balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(amount);
        IERC20(tokenMapping[ticker].tokenAddress).transfer(msg.sender, amount);
    }
}
order test.js
const Dex = artifacts.require("Dex")
const Link = artifacts.require("Link")
const ETH = artifacts.require("ETH")
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()
        let eth = await ETH.deployed()
        await truffleAssert.reverts(
            dex.createLimitOrder(0, web3.utils.fromUtf8("LINK"), 10, 1)
        )
        dex.addToken(web3.utils.fromUtf8("ETH"), eth.address, {from: accounts[0]})
        await eth.approve(dex.address, 1000);
        await dex.deposit(500, web3.utils.fromUtf8("ETH"))
        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()
        let eth = await ETH.deployed()
        dex.addToken(web3.utils.fromUtf8("LINK"), link.address, {from: accounts[0]})
        await truffleAssert.reverts(
            dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 10, 1)
        )
        await link.approve(dex.address, 500);
        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.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)
        for (let i = 0; i < orderbook.length - 1; i++) {
            assert(orderbook[i].price > orderbook[i+1].price)
        }
    })
    //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)
        }
    })
})

contract("Dex", accounts => {
    //When creating a SELL market order, the seller needs to have enough tokens for the trade
    it("Should throw an error when creating a sell market order without adequate token balance", 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"), 10)
        )
    })
    //Market orders can be submitted even if the order book is empty
    it("Market orders can be submitted even if the order book is empty", async () => {
        let dex = await Dex.deployed()
        let eth = await ETH.deployed()
        
        dex.addToken(web3.utils.fromUtf8("ETH"), eth.address, {from: accounts[0]})
        await eth.approve(dex.address, 60000);
        await dex.deposit(50000, web3.utils.fromUtf8("ETH"))

        let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 0); //Get buy side orderbook
        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); //Get 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)


        //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, 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); //Get sell side orderbook
        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); //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 buyer link balance before link purchase
        let balanceBefore = await dex.balances(accounts[0], web3.utils.fromUtf8("LINK"))

        //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 though order was for 50. 
        assert.equal(balanceBefore.toNumber() + 15, balanceAfter.toNumber());
    })

    //The eth balance of the buyer should decrease with the filled amount
    it("The eth balance of the 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.toNumber() - 300, balanceAfter.toNumber());
    })

    //The token balances of the limit order sellers should decrease with the filled amounts.
    it("The token balances of the limit order sellers should 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); //Get 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 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
    xit("Filled limit orders should be removed from the orderbook", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()
        let eth = await ETH.deployed()
        await dex.addToken(web3.utils.fromUtf8("LINK"), link.address)

        //Seller deposits link and creates a sell limit order for 1 link for 300 wei
        await link.approve(dex.address, 500);
        dex.addToken(web3.utils.fromUtf8("ETH"), eth.address, {from: accounts[0]})
        await eth.approve(dex.address, 60000);
        await dex.deposit(50, web3.utils.fromUtf8("LINK"));
        
        await dex.deposit(10000, web3.utils.fromUtf8("ETH"))

        let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1); //Get 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); //Get sell side orderbook
        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); //Get 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); //Get sell side orderbook
        assert.equal(orderbook[0].filled, 0);
        assert.equal(orderbook[0].amount, 3);
    })
    //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]})
        )
    })


})

Some differences compared to Filip’s version:

  • ETH is handled as another token. Saw that Filip was using a depositEth function which I couldn’t find out how it was working until I saw Filip’s final code. So that’s why I implemented ETH as another token. Am not sure if that would be a ‘correct’ approach though.
  • The .filled property of the limit orders is a bool which is set to true when that limit order is fully filled.

Personally found it hard to come up with the tests before coding and thus knowing exaclty how the functions should work (kind of got stuck). So that’s why I used FIlip’s testing code and adapted it slightly to work with the differences mentioned above.

Thanks for the course!

2 Likes

Hi guys, my contract’s logic is as follows:

In order to buy or sell token the user has to deposit ETH or deposit the token. Then he can place a limit order specifying token, price and amount. The contract validates that he has enough ETH/token deposited, and adjusts the balance (as in, if he places an order, the associated funds/token get frozen).

The contract creates the order in the orderbook, and then orders it from highest to lowest (both for Sell or BUY it gets ordered from highest to lowest).

Now the contract checks that there is at least 1 pair of BuY/sell orders. IF so, it starts a while loop, checking that the highest buy order’s [Buyorder[0] price is >= than the lowest sell order (Sellorder[length-1]).

It then determines by which amount to trade, adjusts balances of seller and buyer, and pops the orderbook. This goes on until the highest buyorder price is LOWER than the lowest sell order price

The PlaceMarketOrder function is simple, in the sense that it lets the user choose the full amount, and then creates individual orders with the PlaceOrder function. This stops until the full amount is covered, or if there is no Orderbook anymore.

Files: https://github.com/KilianZumFelde/Dexproject/tree/main/DEX

I have some ideas/questions about how to improve. (not relevant core things, small things)

Its kind of a pain that the user has to approve each time he wants to sell token (he has to approve so that the DEX can transferfrom user to DEX). Can’t this be circumvented somehow? I would like this to somehow happen automatically.

The other is the ETH deposit. Again, this is a nuisance…I gotta deposit, and then trade. I would like to just trade. How could one establish that the place order function automatically “takes” amount*price of ETH from the user?

Hi @KZF

Well done with your project!

Its kind of a pain that the user has to approve each time he wants to sell token (he has to approve so that the DEX can transferfrom user to DEX). Can’t this be circumvented somehow? I would like this to somehow happen automatically.

I don’t agree with you that give the approval to a contract to handle and transfer my funds is a ‘pain’ :joy:
I guess thar if you don’t like it, you can code your own version of the ERC20 contract where you change the approval logic so that it becomes perpetual.

The other is the ETH deposit. Again, this is a nuisance…I gotta deposit, and then trade. I would like to just trade. How could one establish that the place order function automatically “takes” amount*price of ETH from the user?

You can set the function trade() payable so that the user deposits the amount of ether he wants to trade as soon as the function is called.

Cheers,
Dani

1 Like

I did things a bit differently for the market order function where i would pop the array after every time an order was filled. I did not have a filled param as i tried to code it without peeking at the solution and ended up doing it with a bunch of if statements.

anyways heres the code. Its not pretty and it probably costs a shit ton of gas but it works :smiley:

Dex Code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// pragma abicoder v2;

import "./wallet.sol";

contract Dex is Wallet {

    enum Side{
        BUY,
        SELL
    }

    enum Status{
        OPEN,
        PARTIALLY_FILLED,
        FILLED
    }

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

    uint public nextOrderID;

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

    event TradeHistory(address indexed marketTaker, uint id, address indexed marketMaker, Side side, bytes32 tickerToSell, bytes32 tickerToBuy, uint orderAmount, uint orderPrice, Status orderStatus);
    event LimitOrderSubmitted(address indexed trader, uint id, Side side, bytes32 tickerToBuy, bytes32 tickerToSell, uint orderAmount, uint orderPrice, Status orderStatus);

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

    // for this project the amount will be based on ETH, and price will be how much UHU per 1 ETH
    function createLimitOrder(Side side, bytes32 ticker, uint amount, uint price) public {
        // First to place the actual Order, after order is place we will run more code to match the and process the order if price is right
        // Check if order is to buy or sell


        if (side == Side.BUY){ // If order is to BUY
            require (balances[msg.sender][bytes32("ETH")] >= amount * price, "Insufficient token balance to place Buy order");

        }    
        else if (side == Side.SELL){ // If order is to SELL
            require (balances[msg.sender][ticker] >= amount, "Insufficient token balance to place Sell order");
        }

        Order[] storage orders = orderBook[ticker][uint(side)];
        orders.push(Order(nextOrderID, msg.sender, side, ticker, amount, price));
        bytes32 tickerToBuy = side == Side.BUY ? ticker : bytes32("ETH");
        bytes32 tickerToSell = side == Side.SELL ? ticker : bytes32("ETH");
        emit LimitOrderSubmitted(msg.sender, nextOrderID, side, tickerToBuy, tickerToSell, amount, price, Status.OPEN);


        if(orders.length > 1){
            if (side == Side.BUY){ // If order is to BUY
                for (uint i = orders.length-1; i>0; i--){
                    if (orders[i].price < orders[i-1].price ) break;               
                    if (orders[i].price > orders[i-1].price) {
                        Order memory ordersSwap = orders[i];                    
                        orders[i] = orders[i-1];
                        orders[i-1] = ordersSwap;
                    }
                }
            }            
            else if (side == Side.SELL){ // If order is to SELL
                for (uint i = orders.length-1; i>0; i--){
                    if (orders[i].price > orders[i-1].price ) break;
                    if (orders[i].price < orders[i-1].price ) {
                        Order memory ordersSwap = orders[i];                    
                        orders[i] = orders[i-1];
                        orders[i-1] = ordersSwap;
                    }
                }      
            }
        }

        nextOrderID++;
    } // end of limitOrder function   

    function createMarketOrder(Side side, bytes32 ticker, uint _amount) public {
        if (side == Side.BUY){ // If order is to BUY
            require (balances[msg.sender][bytes32("ETH")] >= _amount, "Insufficient token balance to place Buy order");

        }    
        else if (side == Side.SELL){ // If order is to SELL
            require (balances[msg.sender][ticker] >= _amount, "Insufficient token balance to place Sell order");
        }    
        
        uint opSide = side == Side.BUY? 1:0;
        Order[] storage orders = orderBook[ticker][opSide];

        if (side == Side.BUY){ // If order is to BUY
            for (uint i = orders.length-1; i>=0; i--){
                if (_amount < (orders[i].price * orders[i].amount)){
                    balances[msg.sender][bytes32("ETH")] -= _amount;
                    balances[orders[i].trader][ticker] -= (_amount / orders[i].price);
                    balances[msg.sender][ticker] += (_amount / orders[i].price);
                    balances[orders[i].trader][bytes32("ETH")] += _amount;
                    emit TradeHistory(msg.sender, orders[i].id, orders[i].trader, side, ticker, bytes32("ETH"), _amount, orders[i].price, Status.PARTIALLY_FILLED);
                    orders[i].amount -= (_amount / orders[i].price);
                    _amount = 0;
                    break;                      
                }
                else if (_amount == (orders[i].price * orders[i].amount)){  
                    balances[msg.sender][bytes32("ETH")] -= _amount;
                    balances[orders[i].trader][ticker] -= orders[i].amount;
                    balances[msg.sender][ticker] += orders[i].amount;
                    balances[orders[i].trader][bytes32("ETH")] += _amount;
                    emit TradeHistory(msg.sender, orders[i].id, orders[i].trader, side, ticker, bytes32("ETH"), _amount, orders[i].price, Status.FILLED);
                    _amount = 0;
                    orders.pop();

                    break;
                }
                else { // if (_amount > (orders[i].price * orders[i].amount)){
                    uint __amount = (orders[i].price * orders[i].amount);
                    balances[msg.sender][bytes32("ETH")] -= __amount;
                    balances[orders[i].trader][ticker] -= orders[i].amount;
                    balances[msg.sender][ticker] += orders[i].amount;
                    balances[orders[i].trader][bytes32("ETH")] += __amount;
                    emit TradeHistory(msg.sender, orders[i].id, orders[i].trader, side, ticker, bytes32("ETH"), _amount, orders[i].price, Status.FILLED);
                    _amount -= __amount;
                    orders.pop();

                }
                if (i == 0) break; i == 0;
            }
        }
        else if(side == Side.SELL){ // SIDE SELL, have UHU, want to sell for ETH, so the amount is UHU
            for (uint i = 0; i<=orders.length-1; i){ // orders is from buy book
                if (_amount < orders[i].amount){
                    balances[msg.sender][ticker] -= _amount;
                    balances[orders[i].trader][bytes32("ETH")] -= (_amount * orders[i].price);
                    balances[msg.sender][bytes32("ETH")] += (_amount * orders[i].price);
                    balances[orders[i].trader][ticker] += _amount;
                    emit TradeHistory(msg.sender, orders[i].id, orders[i].trader, side, bytes32("ETH"), ticker, _amount, orders[i].price, Status.PARTIALLY_FILLED);
                    orders[i].amount -= _amount;
                    _amount = 0;  
                    break;                      
                }
                else if (_amount == orders[i].amount){  
                    balances[msg.sender][ticker] -= _amount;
                    balances[orders[i].trader][bytes32("ETH")] -= (_amount * orders[i].price);
                    balances[msg.sender][bytes32("ETH")] += (_amount * orders[i].price);
                    balances[orders[i].trader][ticker] += _amount;
                    emit TradeHistory(msg.sender, orders[i].id, orders[i].trader, side, bytes32("ETH"), ticker, _amount, orders[i].price, Status.FILLED);
                    _amount = 0;
                    orders[i].amount =0;
                    for (uint j=i; j<orders.length-1; j++){
                        orders[j] = orders[j+1];
                    }
                    orders.pop();
                    break;
                }
                else if (_amount > orders[i].amount){ //  && orders.length != 0
                    balances[msg.sender][ticker] -= orders[i].amount;
                    balances[orders[i].trader][bytes32("ETH")] -= (orders[i].amount * orders[i].price);
                    balances[msg.sender][bytes32("ETH")] += (orders[i].amount * orders[i].price);
                    balances[orders[i].trader][ticker] += orders[i].amount;
                    emit TradeHistory(msg.sender, orders[i].id, orders[i].trader, side, bytes32("ETH"), ticker, _amount, orders[i].price, Status.FILLED);
                    _amount -= orders[i].amount;
                    orders[i].amount =0;
                    for (uint j=i; j<orders.length-1; j++){
                        orders[j] = orders[j+1];
                    }
                    orders.pop();
                    if (orders.length == 0) break;
                }
            }
        }
    } // end of market order function

// markerOrder function
// for a market sell order:
// order tries to full the order starting from the top of the Buy order
// if the largest buy limit order in order book is not big enough then the market order
// will fill this order, then continue down.
// until we get to the final order in which case we need to deduct the correct amount of the remaing
// market sell from that market buy.
// if the market sell is bigger than the order book then we will need to fill the order as much
// as we can then cancel the remainder of the market sell order.


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

    mapping(bytes32 => Token) public tokenMapping;
    bytes32[] public tokenList;

    constructor () {
        bytes32 ticker = bytes32("ETH");
        tokenMapping[ticker] = Token(ticker, address(this));
        tokenList.push(ticker);        
    }

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

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

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

    function depositEth() external payable {
        balances[msg.sender][bytes32("ETH")] += msg.value;
    }

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

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

    function withdraw(uint amount) external returns (uint){
        require(balances[msg.sender][bytes32("ETH")] >= amount, "ETH Balance not sufficient");
        balances[msg.sender][bytes32("ETH")] -= amount;
        payable(msg.sender).transfer(amount);
        return balances[msg.sender][bytes32("ETH")];
    }

}
Tests Code
const Dex = artifacts.require("Dex");
const YOLO = artifacts.require("YOLO");
const truffleAssert = require("truffle-assertions");

contract("Dex", function (accounts) {

  // LIMIT ORDERS

  it("L1 should throw an error if the ETH balance is too low when creating BUY limit order", async () =>{
    let dex = await Dex.deployed();
    let yolo = await YOLO.deployed();
    let yoloBytes = web3.utils.asciiToHex(await yolo.symbol());
    await truffleAssert.reverts(dex.createLimitOrder(0, yoloBytes, 2, 10));
    let amount = 200;
    await dex.depositEth({value:amount});
    await truffleAssert.passes(dex.createLimitOrder(0, yoloBytes, 2, 10));
  });

  it("L2 should throw an error if the token balance is too low when creating SELL limit order", async () =>{
    let dex = await Dex.deployed();
    let yolo = await YOLO.deployed();
    let yoloBytes = web3.utils.asciiToHex(await yolo.symbol());
    await dex.addToken(yoloBytes, yolo.address, {from:accounts[0]});
    await truffleAssert.reverts(dex.createLimitOrder(1, yoloBytes, 3, 10));
    await yolo.approve(dex.address, 500)
    await dex.deposit(200, yoloBytes);
    await truffleAssert.passes(dex.createLimitOrder(1, yoloBytes, 3, 10));
  });

  it("L3 Buy order book should be ordered from highest to lower price", async () =>{
    let dex = await Dex.deployed();
    let yolo = await YOLO.deployed();
    let yoloBytes = web3.utils.asciiToHex(await yolo.symbol());
    let amount = 3000;
    await dex.depositEth({value:amount});
    await dex.createLimitOrder(0, yoloBytes, 2, 300);
    await dex.createLimitOrder(0, yoloBytes, 2, 100);
    await dex.createLimitOrder(0, yoloBytes, 2, 200);

    let orderbook = await dex.getOrderBook(yoloBytes,0);
    assert(orderbook.length > 0, "no orders in buy order book");
    for (let i = 0; i < orderbook.length-1; i++) {
      assert(orderbook[i].price >= orderbook[i+1].price, "not right order in buy book")
    }
    console.log(orderbook);  
  });
  
  it("L4 Sell order book should be ordered from lowest to highest price", async () =>{
    let dex = await Dex.deployed();
    let yolo = await YOLO.deployed();
    let yoloBytes = web3.utils.asciiToHex(await yolo.symbol());
    let amount = 3000;
    await yolo.approve(dex.address, amount)
    await dex.deposit(amount,yoloBytes);
    await dex.createLimitOrder(1, yoloBytes, 3, 300);
    await dex.createLimitOrder(1, yoloBytes, 3, 100);
    await dex.createLimitOrder(1, yoloBytes, 3, 200);

    let orderbook = await dex.getOrderBook(yoloBytes,1);
    assert(orderbook.length > 0, "no orders in sell order book");
    for (let i = 0; i < orderbook.length-1; i++) {
      assert(orderbook[i].price <= orderbook[i+1].price, "not right order in sell book")
    }
    console.log(orderbook);        
  });

  // MARKET ORDERS

  it("M1 should throw an error if the ETH balance is too low when creating BUY market order", async () =>{
    let dex = await Dex.deployed();
    let yolo = await YOLO.deployed();
    let yoloBytes = web3.utils.asciiToHex(await yolo.symbol());
    await truffleAssert.reverts(dex.createMarketOrder(0, yoloBytes, 1, {from:accounts[2]}));
    let amount = 10;
    await dex.depositEth({from:accounts[2], value:amount});
    await truffleAssert.passes(dex.createMarketOrder(0, yoloBytes, 1, {from:accounts[2]}));
  });

  it("M2 should throw an error if the token balance is too low when creating SELL market order", async () =>{
    let dex = await Dex.deployed();
    let yolo = await YOLO.deployed();
    let yoloBytes = web3.utils.asciiToHex(await yolo.symbol());
    await dex.addToken(yoloBytes, yolo.address, {from:accounts[0]});
    await truffleAssert.reverts(dex.createMarketOrder(1, yoloBytes, 1, {from:accounts[2]}));
    await yolo.approve(dex.address, 500, {from:accounts[2]})
    await dex.deposit(10, yoloBytes, {from:accounts[2]});
    await truffleAssert.passes(dex.createMarketOrder(1, yoloBytes, 1, {from:accounts[2]}));
  });

  it("M3 should be ok to enter a market order if order book is empty, the function will simply cancel the remaining amount", async () =>{
    let dex = await Dex.deployed();
    let yolo = await YOLO.deployed();
    let yoloBytes = web3.utils.asciiToHex(await yolo.symbol());
    let ethBytes = web3.utils.asciiToHex("ETH");
    let orderbook = await dex.getOrderBook(yoloBytes,1);
    let amount = 3000;
    await yolo.approve(dex.address, amount,{from:accounts[1]});
    console.log(await yolo.allowance(accounts[1],dex.address));
    await dex.deposit(amount,yoloBytes,{from:accounts[1]});
    await dex.depositEth({value:amount, from:accounts[1]});
    console.log(orderbook);  
    console.log("account 1 eth: ",(await dex.balances(accounts[1],ethBytes)).toString());
    console.log("account 1 yolo: ",(await dex.balances(accounts[1],yoloBytes)).toString());
    console.log("account 0 eth: ",(await dex.balances(accounts[0],ethBytes)).toString());
    console.log("account 0 yolo: ",(await dex.balances(accounts[0],yoloBytes)).toString());
    await dex.createMarketOrder(0, yoloBytes, 3000, {from:accounts[1]});
    orderbook = await dex.getOrderBook(yoloBytes,1);
    assert(orderbook.length == 0, "still orders in order book");
    console.log("order book after market order: ",orderbook);  
    console.log("account 1 eth: ",(await dex.balances(accounts[1],ethBytes)).toString());
    console.log("account 1 yolo: ",(await dex.balances(accounts[1],yoloBytes)).toString());
    console.log("account 0 eth: ",(await dex.balances(accounts[0],ethBytes)).toString());
    console.log("account 0 yolo: ",(await dex.balances(accounts[0],yoloBytes)).toString());
  });

  it("M4 should be ok to place a market order larger than order book, the function will simply cancel after last order used up. Should adjust buy and seller balance after every order. Should remove filled order from order book.", async () =>{
    let dex = await Dex.deployed();
    let yolo = await YOLO.deployed();
    let yoloBytes = web3.utils.asciiToHex(await yolo.symbol());
    let ethBytes = web3.utils.asciiToHex("ETH");
    let orderbook = await dex.getOrderBook(yoloBytes,0);
    let amount = 3000;
    await yolo.approve(dex.address, amount,{from:accounts[1]});
    await dex.depositEth({from:accounts[1], value:amount});
    await dex.deposit(amount,yoloBytes,{from:accounts[1]});
    console.log("order book before market sell: ",orderbook);  
    console.log("accountA 1 eth: ",(await dex.balances(accounts[1],ethBytes)).toString());
    console.log("accountA 1 yolo: ",(await dex.balances(accounts[1],yoloBytes)).toString());
    console.log("accountA 0 eth: ",(await dex.balances(accounts[0],ethBytes)).toString());
    console.log("accountA 0 yolo: ",(await dex.balances(accounts[0],yoloBytes)).toString());
    let tx = await dex.createMarketOrder(1, yoloBytes, 9, {from:accounts[1]});
    truffleAssert.prettyPrintEmittedEvents(tx);
    orderbook = await dex.getOrderBook(yoloBytes,0);    
    console.log("order book after market sell: ",orderbook);  
    console.log("accountB 1 eth: ",(await dex.balances(accounts[1],ethBytes)).toString());
    console.log("accountB 1 yolo: ",(await dex.balances(accounts[1],yoloBytes)).toString());
    console.log("accountB 0 eth: ",(await dex.balances(accounts[0],ethBytes)).toString());
    console.log("accountB 0 yolo: ",(await dex.balances(accounts[0],yoloBytes)).toString());
    assert(orderbook.length == 0, "still orders in order book");
    // assert(false);
  });

});

Token Code
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../node_modules/@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "../node_modules/@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
import "../node_modules/@openzeppelin/contracts/access/AccessControlEnumerable.sol";
import "../node_modules/@openzeppelin/contracts/utils/Context.sol";

/**
 * @dev {ERC20} token, including:
 *
 *  - ability for holders to burn (destroy) their tokens
 *  - a minter role that allows for token minting (creation)
 *  - a pauser role that allows to stop all token transfers
 *
 * This contract uses {AccessControl} to lock permissioned functions using the
 * different roles - head to its documentation for details.
 *
 * The account that deploys the contract will be granted the minter and pauser
 * roles, as well as the default admin role, which will let it grant both minter
 * and pauser roles to other accounts.
 */
contract YOLO is Context, AccessControlEnumerable, ERC20Burnable, ERC20Pausable {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
    bytes32 public constant CAP_ROLE = keccak256("CAP_ROLE");
    uint256 private _cap;

    /**
     * @dev Grants `DEFAULT_ADMIN_ROLE`, `MINTER_ROLE` and `PAUSER_ROLE` to the
     * account that deploys the contract.
     *
     * See {ERC20-constructor}.
     */
    constructor(string memory name, string memory symbol, uint256 cap_, address _1, address _2, address _3, address _4) ERC20(name, symbol) {
        _mint(msg.sender, 10000);
        _mint(_1, 10000);
        _mint(_2, 10000);
        _mint(_3, 10000);
        _mint(_4, 10000);
        _setupRole(DEFAULT_ADMIN_ROLE, _msgSender());
        _setupRole(MINTER_ROLE, _msgSender());
        _setupRole(PAUSER_ROLE, _msgSender());
        _setupRole(CAP_ROLE, _msgSender());
        require(cap_ > 0, "ERC20Capped: cap is 0");
        _cap = cap_;
    }

    function cap() public view virtual returns (uint256) {
        return _cap;
    }

    function changeCap(uint256 newCap) external {
        require(hasRole(CAP_ROLE, _msgSender()), "ERC20PresetMinterPauser: must have cap role to change cap");
        require(ERC20.totalSupply() <= newCap, "ERC20Capped: new cap cant be less than current total supply");
        _cap = newCap;
    }

    /**
     * @dev Creates `amount` new tokens for `to`.
     *
     * See {ERC20-_mint}.
     *
     * Requirements:
     *
     * - the caller must have the `MINTER_ROLE`.
     */
    function mint(address to, uint256 amount) public virtual {
        require(hasRole(MINTER_ROLE, _msgSender()), "ERC20PresetMinterPauser: must have minter role to mint");
        require(ERC20.totalSupply() + amount <= cap(), "ERC20Capped: cap exceeded");
        _mint(to, amount);
    }

    /**
     * @dev Pauses all token transfers.
     *
     * See {ERC20Pausable} and {Pausable-_pause}.
     *
     * Requirements:
     *
     * - the caller must have the `PAUSER_ROLE`.
     */
    function pause() public virtual {
        require(hasRole(PAUSER_ROLE, _msgSender()), "ERC20PresetMinterPauser: must have pauser role to pause");
        _pause();
    }

    /**
     * @dev Unpauses all token transfers.
     *
     * See {ERC20Pausable} and {Pausable-_unpause}.
     *
     * Requirements:
     *
     * - the caller must have the `PAUSER_ROLE`.
     */
    function unpause() public virtual {
        require(hasRole(PAUSER_ROLE, _msgSender()), "ERC20PresetMinterPauser: must have pauser role to unpause");
        _unpause();
    }

    function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override(ERC20, ERC20Pausable) {
        super._beforeTokenTransfer(from, to, amount);
    }
}

Thanks @filip for an awesome learning series. I can feel my brain creating new neuron links daily when doing these projects.

2 Likes

What I meant was that, IF I initiate a function as a user to place, for example, a sell order, I would like that to imply that the contract automatically gets that (and only that) amount of tokens approved. Of course, I wouldnt like the contract to be able to handle my tokens like a blank check or something, so I definitely agree there. I guess that there is no way, since the approval function of ERC20 needs me to directly interact with that specific token contract.

Yes I thought about that, the payable function. What I wondered is that if i enter as a variable “price” and “amount”, could that automatically be translated to the value that I want to transfer? I assume, since value is very sensitive, it’s something that i as a user always have to input manually.

cheers!

Hey everyone,
I’m just posting a draft of my ideas for the DEX challenge but I’m having a hard time coding it :sweat_smile:

The main idea is to create a DEX that protects smaller traders (trading with a small amount of money) from big traders. This can be achieved through:

  • Having divisions within the platform based on the total ETH balance of an address (This includes the value of tokens owned in ETH)
  • The lowest division will have a maximum value of ETH they are allowed to trade
  • The highest division will have a minimum value of ETH they are allowed to trade
  • Divisions in between will have both minimum and maximum value of ETH they are allowed to trade

I was thinking of adding tokens only accessible to lower divisions to protect them from massive pump-and-dump moves from a big trader

Introducing Staking
Now that the smaller traders are protected, we still want our big traders to keep using the platform by allowing them to stake their tokens (incentives to big traders)

  • Introduce staking privileges once a certain division is reached
  • Add the percentage earned by staking when moving up on divisions

P.S. I have no code to show yet, sorry :v:

2 Likes

Hey there, here’s my code:

Dex.sol

pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;

import “./Wallet.sol”;

contract Dex is Wallet {

using SafeMath for uint;

// Define a side enum for the order books
enum Side {
    BUY, // 0
    SELL // 1
}

// Define a order that will be put in the order book
struct Order {
    uint id;
    address trader;
    Side side;
    bytes32 ticker;
    uint amount;
    uint price;
    uint filled;
}

uint public nextOrderId = 0;

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

function depositETH() public payable {
    balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].add(msg.value);
}

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

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

    // [Order1, Order2, ...] - list of Orders
    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) 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;

    for(uint i = 0; i < orders.length && totalFilled < amount; i++) {
        // How much we can fill from order[i]
        // Update totalFilled
        uint leftToFill = amount.sub(totalFilled);
        uint availableToFill = orders[i].amount.sub(orders[i].filled); // order.amount - order.filled
        uint filled = 0;

        if(availableToFill > leftToFill) {
            filled = leftToFill; // Fill the entire market order
        } else {
            filled = availableToFill; // Fill as much as is available in order[i]
        }

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

        // Execute the trade & shift balances betweein buyer/seller
        // Verify that the buyer has enough ETH to cover the purchase (require)
        if(side == Side.BUY) {
            // Varify that the buyer has enough ETH to cover the purchase (require)
            require(balances[msg.sender]["ETH"] >= filled.mul(orders[i].price), "Not enough ETH");
            // msg.sender is buyer
            // Execute the trade:
            // Transfer ETH from Buyer to Seller
            // Transfer Tokens from Seller to Buyer
            balances[msg.sender][ticker] = balances[msg.sender][ticker].add(filled);
            balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].sub(cost);

            balances[orders[i].trader][ticker] = balances[orders[i].trader][ticker].sub(filled);
            balances[orders[i].trader]["ETH"] = balances[orders[i].trader]["ETH"].add(cost);
        }
        else if(side == Side.SELL) {
            // msg.sender is seller
            // Execute the trade:
            // Transfer ETH from Buyer to Seller
            // Transfer Tokens from Seller to Buyer
            balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(filled);
            balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].add(cost);

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

    // Remove 100% filled orders from the orderbook

    // [
    //     Order(amount=10, filled=10), // - will be removed
    //     Order(amount=100, filled=100), // - will be removed
    //     Order(amount=25, filled=10),
    //     Order(amount=200, filled=0)
    // ]

    while(orders.length > 0 && orders[0].filled == orders[0].amount) {
        // Remove the top element in the orders array by overwriting every element
        // with the next element in the order list
        for(uint i = 0; i < orders.length - 1; i++) {
            orders[i] = orders[i + 1];
        }
        orders.pop();
    }
}

}

Token.sol

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, 1000);
}

}

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

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

// address => token_symbol => amount: balances;
mapping(address => mapping(bytes32 => uint256)) public balances;

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

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 {
    require(balances[msg.sender][ticker] + amount > balances[msg.sender][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) tokenExist(ticker) external {
    require(balances[msg.sender][ticker] >= amount, "Balance not sufficient.");
    
    IERC20(tokenMapping[ticker].tokenAddress).transfer(msg.sender, amount);
    balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(amount);
}

}

dex.test.js

// 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

const Dex = artifacts.require(“Dex”)
const Link = artifacts.require(“Link”)
const truffleAssert = require(“truffle-assertions”)

contract(“Dex”, accounts => {

let dex
let link

before(async () => {
    dex = await Dex.deployed()
    link = await Link.deployed()
})

it("should throw an error if ETH balance is too low when creating BUY limit order", async () => {    
    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)
    )
})

it("should throw an error if token balance is too low when creating SELL limit order", async () => {
    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)
    )
})

it("The BUY order book should be ordered on price from highest to lowest starting at index 0", async () => {
    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")
    }
})

it("The SELL order book should be ordered on price from lowest to highest starting at index 0", async () => {
    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)
    console.log(orderBook);
    for (let i = 0; i < orderBook.length - 1; i++) {
        assert(orderBook[i].price <= orderBook[i+1].price, "not right order in sell book")
    }
})

})

market_order.test.js

const Dex = artifacts.require(“Dex”)
const Link = artifacts.require(“Link”)
const truffleAssert = require(“truffle-assertions”)

contract(“Dex”, accounts => {

let dex
let link

before(async () => {
    dex = await Dex.deployed()
    link = await Link.deployed()
})

// When creating a SELL market order, the seller needs to have enough tokens for the trade
it("Should throw an error when creating a sell market order without adequate token balance", async () => {
    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"), 10)
    )
})

// Market orders can be submitted even if the order book is empty
it("Market orders can be submitted even if the order book is empty", async () => {
    await dex.depositETH({ value: 50000 })
    let orderBook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 0); // Get buy side orderBook
    assert(orderBook == 0, "Buy side Orderbook lenght 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 orderBook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1) // Get 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)

    // 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, 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) // Get sell side orderbook
    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 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, 400, { from: accounts[2] })

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

    // Create market order that could fill more than the enrire 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 link after, even though order was for 50
    assert.equal(balanceBefore.toNumber() + 15, balanceAfter.toNumber())
})

// The eth balance of the buyer should decrease with the filled amount
it("The eth balance of the buyer should decrease with the filled amount", async () => {
    // Seller depsits 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.toNumber() - 300, balanceAfter.toNumber())
})

// The token balances of the limit order sellers should decrease with the filled amounts
it("The token balances of the limit order sellers should decrease with the filled amounts", async () => {
    let orderBook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1) // Get 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 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 before 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 () => {
    await dex.addToken(web3.utils.fromUtf8("LINK"), link.address)

    // Seller deposits link and creates a sell limit order for 1 link for 300 wei
    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) // Get 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) // Get sell side orderbook
    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 orderBook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1) // Get 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) // Get 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 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] })
    )
})

})

wallet.test.js

const Dex = artifacts.require(“Dex”)
const Link = artifacts.require(“Link”)
const truffleAssert = require(“truffle-assertions”)

contract(“Dex”, accounts => {

let dex
let link

before(async () => {
    dex = await Dex.deployed()
    link = await Link.deployed()
})

it("should be possible only for owner to add tokens", async () => {
    await truffleAssert.passes( 
        dex.addToken(web3.utils.fromUtf8("LINK"), link.address, { from: accounts[0] })
    )
    await truffleAssert.reverts( 
        dex.addToken(web3.utils.fromUtf8("LINK"), link.address, { from: accounts[1] })
    )
})

it("should handle deposits correctly", async () => {
    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 () => {
    await truffleAssert.reverts(
        dex.deposit(500, web3.utils.fromUtf8("LINK"))
    ) 
})

it("should handle correct withdrawals correctly", async () => {
    await truffleAssert.passes(
        dex.deposit(100, web3.utils.fromUtf8("LINK"))
    ) 
})

})

There are almost no differences compared with the course version.

Thanks a lot for the amazing course!

2 Likes

Good job @Vitaliy and happy to see that you’ve enjoyed the course :smiley:
If you did not already, take a look at the old solidity 201 course and also the smart contract security one.

Happy learning!
Dani

1 Like