Project - DEX Final Code

Ahhhh Yes, so far ive done mine a little differently but had no idea localStorage even existed haha. And yes you make a good point, i tried making a single function just now but will need to put more time in better understand what is required. I will implement your JS to mine now and let you know if i’ve cracked it. This little update will definitely make my dex feel more professional.

You should make you’re own tutorials Evan haha, very concise i must say.

Thanks again champ! :love_you_gesture:
Ollie

1 Like

hey ollie. No worries.

1 Like

Please see my final Dex code on github. It took me very long to understand how to push to github via vscode, and after deleting all my files by accident, I finally did it! (perhaps add a tutorial for this?)

https://github.com/hoyzach/dex_project.git

In regard to the ‘filled’ variable that Filip utilized in the market order function, I handled it a little differently and perhaps improved it slightly. In Filip’s code, if the market buyer didn’t have enough Eth to fill the order for that instance of the for loop, it would not fill any of the order. I added some code to decrease the ‘filled’ amount to what the buyer could afford if they did not have enough Eth to purchase all that they ordered. Let me know what you think. Thanks!

1 Like

hey @Zaqoy. congrats man. Although in terms of pushing to github, personally you cant beat a good old push from the terminal doing it the old skool way. 3 commands to do it

git add --all
git commit -m "commit message"
git push -u origin main

works like a charm everytime. And just include things you dont want pushed in the gitignore file such as node modules or .secret files

hey wahats up ollie. I actually just after finishing the multisig dapp that i was working on. Spent the past month on it trying to make it as robust as possible it has user logins and users can create multiple wallets and transfer eth and other erc20s. Fairly happy with how it turned out. I have a vide if your curious. How are you getting on with you dex is it near completion

https://vimeo.com/602624622

1 Like

or if the vimeo link doesnt work try
https://clipchamp.com/watch/zH3KAF0r1fW

1 Like

Hey Evan, sorry ive been away for a few days! Mate that looks sick, very professional. You are learning fast by the looks of it.

My dex is completed, thanks for asking. Also my Crypto Kitties DApp (i called mine Funky Felines), is also completed as well as my website. So i am trying to muster the courage to apply for jobs next week. Hopefully my coding ability is up to scratch and someone sees my potential. But who knows.

Either way you have inspired me to create a front end for my multisig wallet too. I will start it next week and have a look through your code for ideas if that’s okay.

The more we learn the more fun coding is haha.
Keep me posted Evan!
@mcgrane5

1 Like

Below is my simple DEX front end. I don’t know how to upload a screen record video on mac so pictures will have to do for now. Man this was fun and not too difficult for anyone thinking of making a front end as well. There could always be improvements so i will continue to update and debug the minor things.

Screenshot 2021-09-16 at 12.56.37 Screenshot 2021-09-16 at 12.56.51 Screenshot 2021-09-16 at 12.57.01 Screenshot 2021-09-16 at 12.57.13 Screenshot 2021-09-16 at 12.57.24 Screenshot 2021-09-16 at 12.57.32 Screenshot 2021-09-16 at 12.57.38 Screenshot 2021-09-16 at 12.57.45 Screenshot 2021-09-16 at 13.05.22

REPO: https://github.com/olfrank/new_Dex_Dapp
Cheers!
Ollie

2 Likes

I have improved mainly way of sorting orders - they are sorted in opposite side so I do not need to sort my order book when I want to remove filled orders (the commented part in Dex.sol).

Dex.sol
pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;

import "./Wallet.sol";

contract Dex is Wallet {
    using SafeMath for uint256;

    enum Side{
        BUY,
        SELL
    }

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

    uint public nextOrderId = 0;

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

    function getOrderBook(bytes32 ticker, Side side) view public returns(Order[] memory){
        return orderBook[ticker][uint(side)];
    }    
    
    function createLimitOrder(Side side, bytes32 ticker, uint tokenAmount, uint price) public {
        Order[] storage orders = orderBook[ticker][uint(side)];
        if(side == Side.BUY){
            require(tokenAmount * price <= balances[msg.sender]["ETH"], "You do not have enough ETH to create an order");  
        }
        else if(side == Side.SELL) {
            require(tokenAmount <= balances[msg.sender][ticker], "You do not have enough tokens to create an order");
        }
        //Bubble sort
        orders.push(Order(nextOrderId, msg.sender, side, ticker, tokenAmount, price, 0)); 
        //_sortOrderBook(ticker, side);
        _sortOrderBook(ticker, side);
        nextOrderId++;
    }

    function createMarketOrder(Side side, bytes32 ticker, uint tokenAmount) public {
        uint orderBookSide; 
        uint totalFilled;
        uint cost;

        if(side == Side.BUY){
            orderBookSide = 1; //SELL
        }
        else{
            require(balances[msg.sender][ticker] >= tokenAmount, "You do not have enough tokens to trade");
            orderBookSide = 0; //BUY
        }
        Order[] storage orders = orderBook[ticker][orderBookSide];
        
        //require(orders.length > 0, "There is not enough liquidity");
        for(uint i = orders.length; (i > 0) && (totalFilled < tokenAmount); i--){
            uint idx = i - 1;
            uint leftToFill = tokenAmount.sub(totalFilled);
            uint availableToFill = orders[idx].amount.sub(orders[idx].filled);
            if(availableToFill > leftToFill){
                orders[idx].filled = orders[idx].filled.add(leftToFill);
            }
            else{ // availableToFill <= leftToFill
                orders[idx].filled = orders[idx].filled.add(availableToFill);
            }
            totalFilled = totalFilled.add(orders[idx].filled);
            cost = orders[idx].filled.mul(orders[idx].price);
            if(Side.BUY == side){
                require(balances[msg.sender]["ETH"] >= cost, "You do not have enough ETH for this trade");                  
                //sender actions
                balances[msg.sender][ticker] = balances[msg.sender][ticker].add(orders[idx].filled);
                balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].sub(cost);
                //trader actions
                balances[orders[idx].trader][ticker] = balances[orders[idx].trader][ticker].sub(orders[idx].filled);
                balances[orders[idx].trader]["ETH"] = balances[orders[idx].trader]["ETH"].add(cost);
            }
            else{
                //sender actions
                balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(orders[idx].filled);
                balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].add(cost);
                //trader actions
                balances[orders[idx].trader][ticker] = balances[orders[idx].trader][ticker].add(orders[idx].filled);
                balances[orders[idx].trader]["ETH"] = balances[orders[idx].trader]["ETH"].sub(cost);
            }
            if(orders.length > 0 && orders[idx].filled == orders[idx].amount){
                orders.pop();
            }
            else if(orders[idx].filled != orders[idx].amount){
                assert(orders[idx].filled == leftToFill); // There are some exploits in calculations
            }
            else{
                assert(orders.length == 0);
            }
        }
        /*//Remove 100% filled orders from the orderbook
        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 (uint256 i = 0; i < orders.length - 1; i++) {
                orders[i] = orders[i + 1];
            }
            orders.pop();
        }*/
    }

    function getEthDexBalance() view public returns(uint){
        return balances[msg.sender]["ETH"];
    }

    function _sortOrderBook(bytes32 ticker, Side side) private {
        Order[] storage orders = orderBook[ticker][uint(side)];
        Order memory tmpOrder;
        if(side == Side.BUY){
            for(uint i=0; i<orders.length-1; i++){
                for(uint j=i+1; j<orders.length; j++){
                    if(orders[i].price > orders[j].price){
                        tmpOrder = orders[i];
                        orders[i] = orders[j];
                        orders[j] = tmpOrder;
                    }
                }
            }
        }
        else if(side == Side.SELL)
        {
            for(uint i=0; i<orders.length-1; i++){
                for(uint j=i+1; j<orders.length; j++){
                    if(orders[i].price < orders[j].price){
                        tmpOrder = orders[i];
                        orders[i] = orders[j];
                        orders[j] = tmpOrder;
                    }
                }
            }
        }
    }
    function _sortOrderBookIvans(bytes32 ticker, Side side) private {
        Order[] storage orders = orderBook[ticker][uint(side)];
        Order memory tmpOrder;
        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;
                }
                tmpOrder = orders[i-1];
                orders[i-1] = orders[i];
                orders[i] = tmpOrder;
                i--;
            }
        }
        else if(side == Side.SELL){
            while(i > 0){
                if(orders[i-1].price < orders[i].price){
                    break;
                }
                tmpOrder = orders[i-1];
                orders[i-1] = orders[i];
                orders[i] = tmpOrder;
                i--;
            }
        }
        
    }
}

Wallet.sol
pragma solidity ^0.8.0;

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

contract Wallet is Ownable{

    using SafeMath for uint256;

    struct Token {
        bytes32 ticker;
        address tokenAddress;
    }

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

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

    mapping(address => mapping(bytes32 => uint256)) public balances; // mapping from wallet address to mapping from token symbol in bytes to balance

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

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

    function withdraw(uint256 amount, bytes32 ticker) external tokenExist(ticker){
        require(balances[msg.sender][ticker] >= amount, "You do not have such amount to withdraw");

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

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

    function getContractBalance() public view returns (uint){
        return address(this).balance;
            }
    
    function withdrawEth(uint amount) external {
        require(balances[msg.sender][bytes32("ETH")] >= amount,'Insuffient balance'); 
        balances[msg.sender][bytes32("ETH")] = balances[msg.sender][bytes32("ETH")].sub(amount);
        msg.sender.call{value:amount}("");
    }
}
dexMarketOrderTest.js
const Dex = artifacts.require("Dex");
const Link = artifacts.require("Link");
const truffleAssert = require("truffle-assertions");
const BUY = 0;
const SELL = 1;


//The user must have ETH deposited such that deposited eth >= buy order value
contract("Dex", accounts => {
    let linkTicker = web3.utils.fromUtf8("LINK");
    //TestCase 1
    it("TC1: Should have enough ethereum to perform buy order", async () => {
        let dex = await Dex.deployed();
        let link = await Link.deployed();
        let balance = await dex.balances(accounts[0], web3.utils.fromUtf8("ETH"));
        assert.equal(0, balance.toNumber(), "Initial ETH balance is not zero");
    
        //Check liquidity error (for now passes)
        await truffleAssert.passes(
            dex.createMarketOrder(BUY, linkTicker, 10, {from: accounts[0]})
        )
        //Check too low amount of ETH error
        await dex.addToken(linkTicker, link.address, {from: accounts[0]});
        await link.transfer(accounts[1], 10, {from: accounts[0]});
        await link.approve(dex.address, 10, {from: accounts[1]});
        await dex.deposit(10, linkTicker, {from: accounts[1]});
        
        await dex.createLimitOrder(SELL, linkTicker, 10, 1, {from: accounts[1]});
        //For debugging
        console.log("ETH BALANCE (before): " + balance.toNumber());
        //console.log(await dex.getOrderBook(linkTicker, SELL));
        //console.log(await dex.getOrderBook(linkTicker, BUY));

        await truffleAssert.reverts(
            dex.createMarketOrder(BUY, linkTicker, 10, {from: accounts[0]})
        );
            
        await dex.depositEth({value: 10});
        //For debugging
        balance = await dex.balances(accounts[0], web3.utils.fromUtf8("ETH"));

        /*let orderBook = await dex.getOrderBook(linkTicker, SELL)
        console.log(await dex.getOrderBook(linkTicker, SELL));
        console.log(orderBook[0].price * orderBook[0].amount);*/
        console.log("ETH BALANCE (after deposit): " + balance.toNumber());
        
        await truffleAssert.passes(
            dex.createMarketOrder(BUY, linkTicker, 10, {from: accounts[0]})
        )
        balance = await dex.balances(accounts[0], web3.utils.fromUtf8("ETH"));
        //console.log(await dex.getOrderBook(linkTicker, SELL));
        console.log("ETH BALANCE (after market order): " + balance.toNumber());
    });
    //TestCase 2
    it("TC2: Should have enough token balance to perform sell order", async () => {
        let dex = await Dex.deployed();
        let link = await Link.deployed();
        let balance = await dex.balances(accounts[2], linkTicker);

        assert.equal(balance.toNumber(), 0, balance, "Initial LINK balance is not zero");

        //Check liquidity error
        await truffleAssert.reverts(
            dex.createMarketOrder(SELL, linkTicker, 10, {from: accounts[1]})
        );
        
        await dex.addToken(linkTicker, link.address, {from: accounts[0]});
        await link.transfer(accounts[2], 10, {from: accounts[0]});
        await link.approve(dex.address, 10, {from: accounts[0]});
        await link.approve(dex.address, 10, {from: accounts[2]});
        await dex.deposit(10, linkTicker, {from: accounts[0]});
        //console.log(await dex.balances(accounts[1],web3.utils.fromUtf8("ETH")));
        //await dex.depositEth({value: 10, from: accounts[1]});
        console.log(await dex.balances(accounts[1],web3.utils.fromUtf8("ETH")));
        console.log(await dex.balances(accounts[2],linkTicker));
        await dex.createLimitOrder(BUY, linkTicker, 10, 1, {from: accounts[1]});

        await truffleAssert.reverts(
            dex.createMarketOrder(SELL, linkTicker, 10, {from: accounts[2]})
        );

        await dex.deposit(10, linkTicker, {from: accounts[2]});
        
        await truffleAssert.passes(
            dex.createMarketOrder(SELL, linkTicker, 10)
        );
    });
    //When creating a SELL market order, the seller needs to have enough tokens for the trade
    it("TC3: 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[3], linkTicker)
        assert.equal( balance.toNumber(), 0, "Initial LINK balance is not 0" );
        
        await truffleAssert.reverts(
            dex.createMarketOrder(1, linkTicker, 10, {from: accounts[3]})
        )
    })
    //Market orders can be submitted even if the order book is empty
    it("TC4: Market orders can be submitted even if the order book is empty", async () => {
        let dex = await Dex.deployed()
        
        await dex.depositEth({value: 50000});

        let orderbook = await dex.getOrderBook(linkTicker, 0); //Get buy side orderbook
        assert(orderbook.length == 0, "Buy side Orderbook length is not 0");
        
        await truffleAssert.passes(
            dex.createMarketOrder(0, linkTicker, 10)
        )
    })
    //Market orders should be filled until the order book is empty or the market order is 100% filled
    it("TC5: 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(linkTicker, 1); //Get sell side orderbook
        assert(orderbook.length == 0, "Sell side Orderbook should be empty at start of test");

        await dex.addToken(linkTicker, 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, linkTicker, {from: accounts[1]});
        await dex.deposit(50, linkTicker, {from: accounts[2]});
        await dex.deposit(50, linkTicker, {from: accounts[3]});

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

        orderbook = await dex.getOrderBook(linkTicker, 1); //Get sell side orderbook
        //console.log(orderbook);
        //Create market order that should fill 2/3 orders in the book
        await dex.createMarketOrder(0, linkTicker, 10);

        orderbook = await dex.getOrderBook(linkTicker, 1); //Get sell side orderbook
        //console.log(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("TC6: Market orders should be filled until the order book is empty", async () => {
        let dex = await Dex.deployed()

        let orderbook = await dex.getOrderBook(linkTicker, 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, linkTicker, 5, 400, {from: accounts[1]})
        await dex.createLimitOrder(1, linkTicker, 5, 500, {from: accounts[2]})

        //check buyer link balance before link purchase
        let balanceBefore = await dex.balances(accounts[0], linkTicker)
        //console.log(balanceBefore);

        //Create market order that could fill more than the entire order book (15 link)
        await dex.createMarketOrder(0, linkTicker, 50);

        //check buyer link balance after link purchase
        let balanceAfter = await dex.balances(accounts[0], linkTicker)
        //console.log(balanceAfter);
        
        //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("TC7: 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, linkTicker, 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, linkTicker, 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("TC8: 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(linkTicker, 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, linkTicker, {from: accounts[2]});

        await dex.createLimitOrder(1, linkTicker, 1, 300, {from: accounts[1]})
        await dex.createLimitOrder(1, linkTicker, 1, 400, {from: accounts[2]})

        //Check sellers Link balances before trade
        let account1balanceBefore = await dex.balances(accounts[1], linkTicker);
        let account2balanceBefore = await dex.balances(accounts[2], linkTicker);

        //Account[0] created market order to buy up both sell orders
        await dex.createMarketOrder(0, linkTicker, 2);

        //Check sellers Link balances after trade
        let account1balanceAfter = await dex.balances(accounts[1], linkTicker);
        let account2balanceAfter = await dex.balances(accounts[2], linkTicker);

        assert.equal(account1balanceBefore.toNumber() - 1, account1balanceAfter.toNumber());
        assert.equal(account2balanceBefore.toNumber() - 1, account2balanceAfter.toNumber());
    })

    //Filled limit orders should be removed from the orderbook
    it("TC9: Filled limit orders should be removed from the orderbook", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()
        await dex.addToken(linkTicker, 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, linkTicker);
        
        await dex.depositEth({value: 10000});

        let orderbook = await dex.getOrderBook(linkTicker, 1); //Get sell side orderbook

        await dex.createLimitOrder(1, linkTicker, 4, 300)
        await dex.createMarketOrder(0, linkTicker, 4);

        orderbook = await dex.getOrderBook(linkTicker, 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("TC10: Limit orders filled property should be set correctly after a trade", async () => {
        let dex = await Dex.deployed()

        let orderbook = await dex.getOrderBook(linkTicker, 1); //Get sell side orderbook
        assert(orderbook.length == 0, "Sell side Orderbook should be empty at start of test");

        await dex.createLimitOrder(1, linkTicker, 5, 300, {from: accounts[1]});
        orderbook = await dex.getOrderBook(linkTicker, 1); //Get sell side orderbook
        //console.log(orderbook);
        await dex.createMarketOrder(0, linkTicker, 2);
        orderbook = await dex.getOrderBook(linkTicker, 1); //Get sell side orderbook
        //console.log(orderbook);
        assert.equal(orderbook[orderbook.length-1].filled, 2);
        assert.equal(orderbook[orderbook.length-1].amount, 5);
    })
    //When creating a BUY market order, the buyer needs to have enough ETH for the trade
    it("TC11: 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, linkTicker, 5, 300, {from: accounts[1]})

        await truffleAssert.reverts(
            dex.createMarketOrder(0, linkTicker, 5, {from: accounts[4]})
        )
    })

})
1 Like

Had a blast with this course, here is the repo: https://github.com/charliedevcode/dex
Didn’t add any additionals yet, will enjoy more courses in the academy :tophat:

1 Like

I’m new to coding. This is the first time I’ve tried to code anything this complex. I did it my own way, not sure if anything is improved, probably uses more gas, but hey it works:
https://github.com/AttissNgo/solidity-201

1 Like

I learned so much, maximizing the learning experience by taking my time to figure things out and implementing the solution before proceeding to the next video (where you can check with Filip’s code if you got it right). I really enjoyed the course :slight_smile: with great sense of fulfillment.
Here’s my final project code (just the Dex and the latter set of the tests, did not add any additional improvements. I will continue my journey to the next course :slight_smile:

pragma solidity ^0.8.0;

import "./Wallet.sol";

contract Dex is Wallet {

    using SafeMath for uint;

    enum Side {
        BUY,
        SELL
    }

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

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

    uint public nextOrderId = 0;

    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[_msgSender()]["ETH"] >= amount.mul(price));
        } else if (side == Side.SELL) {
            require(balances[_msgSender()][ticker] >= amount);
        }

        Order[] storage orders = orderBook[ticker][uint(side)];
        // Order.filled is by default 0
        orders.push(
            Order(nextOrderId, _msgSender(), side, ticker, amount, price, 0)
        );

        //Sort orderbook with newly added order
        sortOrderbook(side, orders);
        nextOrderId++;
    }

    function createMarketOrder(Side side, bytes32 ticker, uint amount) public {
        
        if (side == Side.SELL) {
            require(balances[_msgSender()][ticker] >= amount, "Not enough token balance to sell");
        }

        uint orderBookSide;
        orderBookSide = (side == Side.BUY ? 1 : 0);

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

        uint totalFilled;
        uint availableAmount;  // How much we can fill from order[i]
        uint amountFilled;     // Amount filled from an order to be paid and transferred
        uint neededAmount;     // Remaining amount to be filled for a market order

        for (uint256 i = 0; i < orders.length && totalFilled < amount; i++) {

            availableAmount = (orders[i].amount).sub(orders[i].filled);
            neededAmount = amount.sub(totalFilled);

            // Update totalFilled
            if (neededAmount >= availableAmount) {
                amountFilled = availableAmount;
            } else {
                amountFilled = neededAmount;
            }
            totalFilled = totalFilled.add(amountFilled);
            orders[i].filled = (orders[i].filled).add(amountFilled);

            // For better readability, calculate total cost (amount * price)
            uint totalCost = amountFilled.mul(orders[i].price);

            if (side == Side.BUY) {
                // Verify that the buyer has enough ETH to cover the purchase (require)
                require(balances[_msgSender()]["ETH"] >= totalCost);

                // Execute the trade, shift balances between buyer and seller
                balances[_msgSender()]["ETH"] = balances[_msgSender()]["ETH"].sub(totalCost);
                balances[orders[i].trader]["ETH"] = balances[orders[i].trader]["ETH"].add(totalCost);

                balances[_msgSender()][ticker] = balances[_msgSender()][ticker].add(amountFilled);
                balances[orders[i].trader][ticker] = balances[orders[i].trader][ticker].sub(amountFilled);
            } else {
                // Execute the trade, shift balances between seller and buyer
                balances[_msgSender()]["ETH"] = balances[_msgSender()]["ETH"].add(totalCost);
                balances[orders[i].trader]["ETH"] = balances[orders[i].trader]["ETH"].sub(totalCost);
                
                balances[_msgSender()][ticker] = balances[_msgSender()][ticker].sub(amountFilled);
                balances[orders[i].trader][ticker] = balances[orders[i].trader][ticker].add(amountFilled);
            }
        }

        // Loop through the orderbook and delete 100% filled orders, sort, then remove filled orders
        for (uint x = 0; x < orders.length; ) {
            if (orders[x].filled == orders[x].amount) {
                for (uint y = x; y < orders.length - 1; y++) {
                    orders[y] = orders[y + 1];
                }
                orders.pop();
            } else {
                break;
            }
        }
    }

    function sortOrderbook(Side side, Order[] storage orders) private {
        Order memory tempOrder;

        if (side == Side.BUY) {
            // [5,2,3] => [5,3,2]
            for (uint i = orders.length - 1; i > 0; i--) {
                if (orders[i].price > orders[i - 1].price) {
                    tempOrder = orders[i -1];
                    orders[i - 1] = orders[i];
                    orders[i] = tempOrder;
                } else {
                    break;
                }
            }
        } else if (side == Side.SELL) {
            // [2,5,3] = [2,3,5]
            for (uint i = orders.length - 1; i > 0; i--) {
                if (orders[i].price < orders[i - 1].price) {
                    tempOrder = orders[i -1];
                    orders[i - 1] = orders[i];
                    orders[i] = tempOrder;
                } else {
                    break;
                }
            }
        }
    }
}
const Link = artifacts.require("Link");
const Dex = artifacts.require("Dex");
const truffleAssert = require('truffle-assertions');

// Market Order Tests
// Parameters: Order.side, Order.ticker, Order.amount
contract("Dex", accounts => {

    // When creating a SELL market order, the seller needs to have enough tokens for the trade
    it("should not allow a seller to create a SELL market order not having enough tokens for the trade", async() => {
        let dex = await Dex.deployed();
        let link = await Link.deployed();

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

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

        await dex.deposit(web3.utils.fromUtf8("LINK"), 10);

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

    // When creating a BUY market order, the buyer needs to have enough ETH for the trade
    it("should not allow a buyer to create a BUY market order not having enough ETH for the trade", async() => {
        let dex = await Dex.deployed();
     
        await dex.deposit(web3.utils.fromUtf8("LINK"), 10);
        await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 10, 1);

        await truffleAssert.reverts(
            dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 10)
        );

        await dex.depositEth(10);

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

    // Market orders can be submitted even if the order book is empty
    it("should allow market order to be submitted even if the order book is empty", async() => {
        let dex = await Dex.deployed();

        await dex.deposit(web3.utils.fromUtf8("LINK"), 10);      

        // SELL market order
        await truffleAssert.passes(
            dex.createMarketOrder(1, web3.utils.fromUtf8("LINK"), 10)
        );

        // BUY market order
        await truffleAssert.passes(
            dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 10)
        );
    });

    //  SELL market orders should be filled until the order book is empty or the market order is 100% filled
    it("should fill SELL market order until 100% or order book is empty", async() => {
        let dex = await Dex.deployed();

        await dex.deposit(web3.utils.fromUtf8("LINK"), 30);
        await dex.depositEth(30);
        
        // BUY limit order
        await dex.createLimitOrder(0, web3.utils.fromUtf8("LINK"), 10, 1);
        await dex.createLimitOrder(0, web3.utils.fromUtf8("LINK"), 10, 1);
        await dex.createLimitOrder(0, web3.utils.fromUtf8("LINK"), 10, 1);

        // SELL market order 100% filled
        await dex.createMarketOrder(1, web3.utils.fromUtf8("LINK"), 20);

        let orders = await dex.getOrderbook(web3.utils.fromUtf8("LINK"), 0);
        assert(orders.length == 1, "Market order should be filled leaving 1 remaining limit order");

        // SELL market order, order book empty
        await dex.createMarketOrder(1, web3.utils.fromUtf8("LINK"), 20);

        orders = await dex.getOrderbook(web3.utils.fromUtf8("LINK"), 0);
        assert(orders.length == 0, "Order book should be empty");
    });

    //  BUY market orders should be filled until the order book is empty or the market order is 100% filled
    it("should fill BUY market order until 100% or order book is empty", async() => {
        let dex = await Dex.deployed();

        await dex.deposit(web3.utils.fromUtf8("LINK"), 30);
        await dex.depositEth(10);

        // SELL limit order
        await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 10, 1);
        await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 10, 1);
        await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 10, 1);

        // BUY market order 100% filled
        await dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 20);

        let orders = await dex.getOrderbook(web3.utils.fromUtf8("LINK"), 1);
        assert(orders.length == 1, "Market order should be filled leaving 1 remaining limit order");

        // BUY market order, order book empty
        await dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 20);

        orders = await dex.getOrderbook(web3.utils.fromUtf8("LINK"), 1);
        assert(orders.length == 0, "Order book should be empty");
    });

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

        await link.transfer(accounts[1], 100);

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

        await dex.depositEth(20);

        let balanceBefore = await dex.balances(accounts[0], web3.utils.fromUtf8("ETH"));
        // BUY market order
        await dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 10);
        let balance = await dex.balances(accounts[0], web3.utils.fromUtf8("ETH"));
        assert.equal(balanceBefore - 20, balance); 
    });

    // The token balances of the sellers should decrease with the filled amounts.
    it("should decrease token balance of seller with filled amount", async() => {
        let dex = await Dex.deployed();
        let link = await Link.deployed();

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

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

        await dex.depositEth(10);
        let balanceBefore = await dex.balances(accounts[1], web3.utils.fromUtf8("LINK"));
        // BUY market order
        await dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 10);

        let balance = await dex.balances(accounts[1], web3.utils.fromUtf8("LINK"));
        assert.equal(balanceBefore - 10, balance); 
    });

    // Filled limit orders should be removed from the orderbook
    it("should remove filled limit orders from the orderbook", async() => {
        let dex = await Dex.deployed();
        let link = await Link.deployed();

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

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

        await dex.depositEth(10);
        // one BUY market order should be removed
        await dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 10);

        let orders = await dex.getOrderbook(web3.utils.fromUtf8("LINK"), 1);
        assert.equal(orders.length, 0); 
    });

    // 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 link = await Link.deployed();

        await dex.addToken(web3.utils.fromUtf8("LINK"), link.address);
        await link.transfer(accounts[1], 10);
        await link.approve(dex.address, 10, {from: accounts[1]});
        await dex.deposit(web3.utils.fromUtf8("LINK"), 10, {from: accounts[1]});

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

        await dex.depositEth(10);
        await dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 5);

        let orders = await dex.getOrderbook(web3.utils.fromUtf8("LINK"), 1);
        assert.equal(orders[0].filled, 5); 
        assert.equal(orders[0].amount, 10);
    });    

});
1 Like

Wallet

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;
    //Mapping address to tokenID to balance
    mapping(address =>mapping(bytes32 => uint)) public balances;

    modifier tokenExist(bytes32 ticker_){
        require(tokenMapping[ticker_].tokenAddress != address(0), "exchange doesn't support token");
        _;
    }
    function addToken(bytes32 ticker_, address tokenAddress_) external onlyOwner {
        if(tokenMapping[ticker_].tokenAddress == address(0)){
            tokenMapping[ticker_] = Token(ticker_,tokenAddress_);
            tokenList.push(ticker_);
        }
        else {
            tokenMapping[ticker_] = Token(ticker_,tokenAddress_);
        }
    }
    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_, "not enough balance");
        balances[msg.sender][ticker_] -= amount_;
        IERC20(tokenMapping[ticker_].tokenAddress).transfer(msg.sender, amount_);
    }

    function depositEth() external payable{
        balances[msg.sender]["ETH"] += msg.value;
    }
    function withdrawEth(uint amount_) external {
        require(balances[msg.sender]["ETH"] >= amount_, "You are trying to withdraw more than you have!");
        balances[msg.sender]["ETH"] -= amount_;
        payable(msg.sender).transfer(amount_);
    }
}

Dex

pragma solidity ^0.8.0;

import "./wallet.sol";

contract Dex is Wallet{

    enum Side { BUY, SELL }
    struct Order {
        uint id;
        address trader;
        bytes32 ticker_;
        Side side_;
        uint amount_;
        uint price; 
        uint filled;
    }
    uint public nextOrderId = 0;

    mapping (bytes32=>mapping(uint =>Order[])) orders;

    function getOrderBook(bytes32 ticker_, Side side_) public view returns (Order[] memory){
        return orders[ticker_][uint(side_)];
    }

    function createLimitOrder(Side side_, bytes32 ticker_, uint amount__, uint price_) public {
        if(side_ == Side.BUY){
            require(balances[msg.sender]["ETH"] >= amount__ * price_, "BUY ORDER: not enought eth to place order!");
        }
        else if(side_ == Side.SELL){
            require(balances[msg.sender][ticker_] >= amount__, "SELL ORDER: not enought tokens to place order!");

        }

        Order[] storage orders_ = orders[ticker_][uint(side_)];

        orders_.push(Order(nextOrderId,msg.sender,ticker_, side_, amount__, price_, 0));
        Order memory _order;
        //Bubble sort
        if(side_ == Side.BUY){
            for(uint i = orders_.length -1; i > 0; --i){
                uint nextIndex = i-1;
                if(orders_[i].price > orders_[nextIndex].price){
                    _order = orders_[i];
                    orders_[i] = orders_[nextIndex];
                    orders_[nextIndex] = _order;
                }
            }
        }
        else if(side_ == Side.SELL){
            for(uint i = orders_.length -1; i > 0; --i){
                uint nextIndex = i-1;
                if(orders_[i].price < orders_[nextIndex].price){
                    _order = orders_[i];
                    orders_[i] = orders_[nextIndex];
                    orders_[nextIndex] = _order;
                }
            }
        }
        ++nextOrderId;
    }
    function createMarketOrder(Side side_, bytes32 ticker_, uint amount_) public {
        if(side_ == Side.SELL){
            require(balances[msg.sender][ticker_] >= amount_, "Insuffient balance");
        }
        
        uint orderBookSide;
        if(side_ == Side.BUY){
            orderBookSide = 1;
        }
        else{
            orderBookSide = 0;
        }
        Order[] storage _orders = orders[ticker_][orderBookSide];

        uint totalFilled = 0;

        for (uint256 i = 0; i < _orders.length && totalFilled < amount_; i++) {
            uint leftToFill = amount_ - totalFilled;
            uint availableToFill = _orders[i].amount_ - _orders[i].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 + filled;
            _orders[i].filled = _orders[i].filled + filled;
            uint cost = filled * _orders[i].price;

            if(side_ == Side.BUY){
                //Verify that the buyer has enough ETH to cover the purchase (require)
                require(balances[msg.sender]["ETH"] >= cost);
                //msg.sender is the buyer
                balances[msg.sender][ticker_] = balances[msg.sender][ticker_] + filled;
                balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"] - cost;
                
                balances[_orders[i].trader][ticker_] = balances[_orders[i].trader][ticker_] - filled;
                balances[_orders[i].trader]["ETH"] = balances[_orders[i].trader]["ETH"] + cost;
            }
            else if(side_ == Side.SELL){
                //Msg.sender is the seller
                balances[msg.sender][ticker_] = balances[msg.sender][ticker_] - filled;
                balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"] + cost;
                
                balances[_orders[i].trader][ticker_] = balances[_orders[i].trader][ticker_] + filled;
                balances[_orders[i].trader]["ETH"] = balances[_orders[i].trader]["ETH"] - cost;
            }
            
        } 
        while(_orders.length > 0 && _orders[0].filled == _orders[0].amount_){ 
            for (uint256 i = 0; i < _orders.length - 1; i++) {
                _orders[i] = _orders[i + 1];
            }
            _orders.pop();
        }
    }
}

Test

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

contract("Dex", (accounts) => {
  //When creating a SELL market order, the seller needs to have enough tokens for the trade
  it("Should throw an error 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();

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

    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();
    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 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, 2);
    assert.equal(orderbook[0].amount, 5);
  });
  //When creating a BUY market order, the buyer needs to have enough ETH for the trade
  it("Should throw an error when creating a buy market order without adequate ETH balance", async () => {
    let dex = await Dex.deployed();

    let balance = await dex.balances(accounts[4], web3.utils.fromUtf8("ETH"));
    assert.equal(balance.toNumber(), 0, "Initial ETH balance is not 0");
    await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 5, 300, {
      from: accounts[1],
    });

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

Tests

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

contract("Dex", accounts => {

    it("Buyer should have enough ETH deposited on his account", async() => {

        let dex = await Dex.deployed()
        //let link = await Link.deployed()

        let balance = await dex.balances(accounts[0], web3.utils.fromUtf8("ETH"))

        
        assert.equal( balance.toNumber(), 0 )
            
        
        await dex.depositEth({value: 10})
        await truffleAssert.passes(
            dex.createMarketOrder(web3.utils.fromUtf8("LINK"),10,1,0) 
            )
        
        
    })
    it("Seller should have enough LINK deposited on his account", async() => {

        let dex = await Dex.deployed()
        let link = await Link.deployed()
        link.approve(dex.address, 500);
        dex.addToken(web3.utils.fromUtf8("LINK"), link.address, {from: accounts[0]})
        let balance = await dex.balances(accounts[0], web3.utils.fromUtf8("LINK"))

     
        assert.equal( balance.toNumber(), 0 )
            
  
        await dex.deposit(10, web3.utils.fromUtf8("LINK"))
        await truffleAssert.passes(
            dex.createMarketOrder(web3.utils.fromUtf8("LINK"),10,1,0) 
            )
    })
    it("Orders can be submitted when order book is empty", async() => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()
        link.approve(dex.address, 500);
        dex.addToken(web3.utils.fromUtf8("LINK"), link.address, {from: accounts[0]})
        let balance = await dex.balances(accounts[0], web3.utils.fromUtf8("LINK"))
        let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 0);

        await dex.deposit(10, web3.utils.fromUtf8("LINK"))
        await truffleAssert.passes(
            assert(orderbook.length == 0, "Buy side Orderbook length not 0"),
            dex.createMarketOrder(web3.utils.fromUtf8("LINK"),10,1,0) 
            )
    })

    //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, 30, {from: accounts[1]})
        await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 5, 40, {from: accounts[2]})
        await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 5, 50, {from: accounts[3]})

        //Create market order that should fill 2/3 orders in the book
        await dex.createMarketOrder(web3.utils.fromUtf8("LINK"),10,1,1) 
        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)
        dex.createMarketOrder(web3.utils.fromUtf8("LINK"),50,1,1) 

        //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(web3.utils.fromUtf8("LINK"), 300, 1, 0);
        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
        dex.createMarketOrder(web3.utils.fromUtf8("LINK"),10,2,0) 

        //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()
        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(web3.utils.fromUtf8("LINK"),300,1,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(web3.utils.fromUtf8("LINK"),10,1,0) 

        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 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(web3.utils.fromUtf8("LINK"),10,1,0) 
        )
    })


})

Dex

pragma solidity 0.8.10;
pragma experimental ABIEncoderV2;
import “…/contracts/Wallet.sol”;

contract Dex is Wallet {

uint public nextOrderId;

using SafeMath for uint256;

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

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

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


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

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

    //Bubble sort
    uint o = orders.length;

    if(side == Side.BUY) {
        for (uint i=o-1;i>0;i--){
        if(orders[i].price > orders[i-1].price) {
        Order memory ordswitch = orders[i];
        orders[i] = orders[i-1];
        orders[i-1] = ordswitch;
        }else{
            break;
        }
        }
   
    }  
    else if(side == Side.SELL){
        for (uint i=o-1;i>0;i--){
        if(orders[i].price < orders[i-1].price) {
        Order memory ordswitch = orders[i];
        orders[i] = orders[i-1];
        orders[i-1] = ordswitch;
        }else{
            break;
        }
        }
    }
}

function createMarketOrder(bytes32 ticker, uint price, uint amount, Side side) public {
       uint orderBookSide;
       if(side == Side.BUY){
           require(balances[msg.sender]["ETH"] >= amount, "Not enough balance");
           orderBookSide = 1;
       }
       else{
           require(balances[msg.sender][ticker] >= amount, "Not enough balance");
           orderBookSide = 0;
       }
       Order[] storage orders = orderBook[ticker][orderBookSide];
       
       uint totalFilled;

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

           totalFilled = totalFilled.add(filled);
           orders[i].filled = orders[i].filled.add(filled);
           uint cost = filled.mul(orders[i].price);
       }
       
    while(orders.length > 0 && orders[0].filled == orders[0].amount){
           // Overwrite the top element with the next element in the list of orders 
           for (uint256 i = 0; i < orders.length - 1; i++) {
               orders[i] = orders[i + 1];
           }
        orders.pop();
    }
}

}

1 Like

Wallet.sol

// SPDX-License-Identifier: UNLICENSED

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

pragma solidity ^0.8.0;

contract Wallet is Ownable {

    using SafeMath for uint;

    struct Token {
        bytes32 ticker;
        address tokenAddress;
    }

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

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

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

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

        balances[msg.sender][ticker] = balances[msg.sender][ticker].add(amount);
    }

    function depositEth() external payable {
        balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].add(msg.value);
    }
    
    function withdraw(uint amount, bytes32 ticker) external {
        require(tokenMapping[ticker].tokenAddress != address(0), "Wallet: token does not exist");
        require(balances[msg.sender][ticker] >= amount, "Wallet: insufficient token balance");

        balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(amount);

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

Dex.sol

// SPDX-License-Identifier: UNLICENSED

import "./Wallet.sol";

pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;

contract Dex is Wallet {

    using SafeMath for uint;

    enum Side {
        BUY,
        SELL
    }

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

    uint public nextOrderId;

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

    function getOrderbook(bytes32 ticker, Side side) public view returns(Order[] memory){
        return orderbook[ticker][uint(side)];
    }
    
    function createLimitOrder(Side side, bytes32 ticker, uint amount, uint price) public {
        if(side == Side.BUY){
            require(balances[msg.sender]["ETH"] >= amount.mul(price), "Insufficient ether balance");
        }

        else if(side == Side.SELL){
            require(balances[msg.sender][ticker] >= amount, "Insufficient token balance"); 
        }

        Order[] storage orders = orderbook[ticker][uint(side)];
        orders.push(
            Order(nextOrderId, msg.sender, side, ticker, amount, price, 0)
        );

        // Bubble sort used below.

        uint i = 0;

        if(orders.length > 0){
            i = orders.length-1;
        }
        
        else {
            i = 0;
        }

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

        // If orders.length is larger than 0, i will equal
        // orders.length-1, otherwise i will equal 0.

        if(side == Side.BUY){
            while(i > 0){
                if(orders[i-1].price > orders[i].price){
                    break;
                }
                Order memory orderToSwap = orders[i-1];
                orders[i-1] = orders[i];
                orders[i] = orderToSwap;
                i--;
            }
        }
        
        else if(side == Side.SELL){
            while(i > 0){
                if(orders[i-1].price < orders[i].price){
                    break;
                }
                Order memory orderToSwap = orders[i-1];
                orders[i-1] = orders[i];
                orders[i] = orderToSwap;
                i--;
            }
        }
        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 if(side == Side.SELL){
            orderbookSide = 0;
        }

        Order[] storage orders = orderbook[ticker][orderbookSide];

        uint totalFilled;

        for(uint i = orders.length-1; i > 0 && totalFilled < amount; i--){
            uint leftToFill = amount.sub(totalFilled);                 // Remaining amount to be filled in function.
            uint leftInOrder = orders[i].amount.sub(orders[i].filled); // Remaining amount to be filled in orders[i]
            address trader = orders[i].trader;
            uint filled;

            if(leftInOrder > leftToFill){
                filled = leftToFill;  // Fill msg.sender's order entirely
            }                         // and partially fill orders[i].

            else {
                filled = leftInOrder; // Fill what is left in orders[i] and
            }                         // partially fill msg.sender's order.

            totalFilled.add(filled);
            orders[i].filled.add(filled);

            if(orderbookSide == 1){
                require(balances[msg.sender]["ETH"] >= filled.mul(orders[i].price));
                
                balances[msg.sender]["ETH"].sub(filled.mul(orders[i].price));
                balances[trader]["ETH"].add(filled.mul(orders[i].price));

                balances[msg.sender][ticker].add(filled);
                balances[trader][ticker].sub(filled);
            }

            else if(orderbookSide == 0){
                balances[msg.sender][ticker].sub(filled);
                balances[trader][ticker].add(filled);

                balances[msg.sender]["ETH"].add(filled.mul(orders[i].price));
                balances[trader]["ETH"].sub(filled.mul(orders[i].price));
            }
        }
        // Loop through the array and remove any fully filled orders.
        for(uint i = orders.length-1; i > 0; i--){
            if(orders[i].filled == orders[i].amount){
                break;
            }
            orders.pop();
        }
    }
}

Would just like to ask if my Dex.sol works correctly considering I used different integers in the for loops in the market order function.

Thanks. :+1:

1 Like

I have a question about this project. What is the difference between Msg.sender and address(this) ?

Also what does ticker represent , I don’t really get ?

msg.sender represents the account that will trigger the function, while address(this) represent the address of the contract.

Here is a quick example for it that you can quickly run in remix. :nerd_face:

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.10;

contract Example {

    function getAddressThis () public view returns(address){
        return address(this);
    }
    
    function getMsgAddress () public view returns(address){
        return address(msg.sender);
    }

}

Carlos Z

Thanks man.
I’m confused about an NFT project.
The user says “He’s launching an NFT collection and needs a contract for it and a minting website”
And also asked that I create a sample with the meta data he sent to me.

1 Like

Here is my final project.

Hope this is of satisfaction, but if anything could be improved or changed, please let me know. Any advice is welcomed. TIA :+1:

Wallet.sol

wallet.sol
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

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

import "../node_modules/@openzeppelin/contracts/utils/math/SafeMath.sol";

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

contract Wallet is Ownable {

    using SafeMath for uint256;

    struct Token {

        bytes32 ticker;

        address tokenAddress;

    }

    mapping(bytes32 => Token) public tokenMapping;

    bytes32[] public tokenList;

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

    modifier tokenExists(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) tokenExists(ticker) external {

        // transfer tokens to caller

        IERC20(tokenMapping[ticker].tokenAddress).transferFrom(msg.sender, address(this), amount);

        balances[msg.sender][ticker] = balances[msg.sender][ticker].add(amount); //add token

    }

    function withdraw(uint amount, bytes32 ticker) tokenExists(ticker) external {

        require(balances[msg.sender][ticker] >= amount, "Insufficient funds"); //does owner have enough funds

       

        balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(amount);

        IERC20(tokenMapping[ticker].tokenAddress).transfer(msg.sender, amount);

    }

}

Dex.sol

dex.sol
// SPDX-License-Identifier: MIT

pragma solidity >=0.6.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;

        bool buyOrder;

        bytes32 ticker;

        uint amount;

        uint price;

        uint filled;

    }

    uint public nextOrderId = 0;

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

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

        return orderBook[ticker][uint(side)];

    }

    function createLimitOrder(Side side, bytes32 ticker, uint amount, uint price) public {

        if(side == Side.BUY) {

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

        }

        else if(side == Side.SELL) {

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

        }

        Order[] storage orders = orderBook[ticker][uint(side)];

        orders.push(

            Order(nextOrderId, msg.sender, side, ticker, amount, price, 0)

        );

     

        //bubble sort

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

        if(side == Side.BUY) {

            //loop

            while(i > 0) {

                if(orders[i - 1].price > orders[i].price) {

                    break;

                }

                Order memory orderMovement = orders[i - 1];

                orders[i - 1] = orders[i];

                orders[i] = orderMovement;

                i--;

            }

        }

        else if(side == Side.SELL) {

            //loop

            while(i > 0) {

                if(orders[i - 1].price < orders[i].price) {

                    break;

                }

                Order memory orderMovement = orders[i - 1];

                orders[i - 1] = orders[i];

                orders[i] = orderMovement;

                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 = 0;

        for (uint256 i = 0; i < orders.length && totalFilled < amount; i++) {

            uint leftToFill = amount.sub(totalFilled);

            uint availableToFill = orders[i].amount.sub(orders[i].filled);

            uint filled = 0;

            //How much we can fill from order[i]

            if(availableToFill > leftToFill) {

                filled = leftToFill; //fill the entire market from order[i]

            }

            else {

                filled = availableToFill; //fill as much as is available in order[i]

            }

            //update totalFilled

            totalFilled = totalFilled.add(filled);

            orders[i].filled = orders[i].filled.add(filled);

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

            //Execute the trade and shift balance between buyer/seller

            if(side == Side.BUY) {

                //Verify that the buyer has enough ETH to cover the purchase (require)

                require(balances[msg.sender]["ETH"] >= cost);

                //msg.sender is the buyer

                address buyer = msg.sender;

                //Transfer ETH from buyer to seller

                balances[buyer]["ETH"] = balances[buyer]["ETH"].sub(cost);

                //Transfer tokens from seller to buyer

                balances[buyer][ticker] = balances[buyer][ticker].add(filled);

                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 the seller

                address seller = msg.sender;

                //Transfer ETH from buyer to seller

                balances[seller]["ETH"] = balances[seller]["ETH"].add(cost);

                //Transfer tokens from seller to buyer

                balances[seller][ticker] = balances[seller][ticker].sub(filled);

                balances[orders[i].trader][ticker] = balances[orders[i].trader][ticker].add(filled);

                balances[orders[i].trader]["ETH"] = balances[orders[i].trader]["ETH"].sub(cost);

            }

        }

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

        while(orders.length > 0 && orders[0].amount == orders[0].filled) {

            //remove the top element in the orders array by overwriting every element

            //with the next element in the orders list

            for (uint256 i = 0; i < orders.length - 1; i++) {

                orders[i] = orders[i + 1];

            }

            orders.pop();

        }

    }

}

Wallet Test

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

contract("Dex", accounts => {
    it("should only be possible for owner to add tokens", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()
        await truffleAssert.passes(
            dex.addToken(web3.utils.fromUtf8("LINK"), link.address, {from: accounts[0]})
        )
        await truffleAssert.reverts(
            dex.addToken(web3.utils.fromUtf8("LINK"), link.address, {from: accounts[1]})
        )
    })
    it("should handle deposits correctly", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()
        await link.approve(dex.address, 500);
        await dex.deposit(100, web3.utils.fromUtf8("LINK"));
        let balance = await dex.balances(accounts[0], web3.utils.fromUtf8("LINK"));
        assert.equal(balance.toNumber(), 100);
    })
    it("should handle faulty withdrawals correctly", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()
        await truffleAssert.reverts(dex.withdraw(500, web3.utils.fromUtf8("LINK")));
    })
    it("should handle correct withdrawals correctly", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()
        await truffleAssert.passes(dex.withdraw(100, web3.utils.fromUtf8("LINK")));
    })
})

Dex Test

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

contract("Dex", accounts => {
    //When creating a SELL market order, the seller needs to have enough tokens for the trade
    it("Should throw an error 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()
        
        await dex.depositEth({value: 50000});

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

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


})

Hopefully I can do more to this in time with frontend and turn it into a dapp. Definitely need to learn more first :smile:

2 Likes

I advice you to take a look into the Ethereum Dapp Programming course, it will teach you how to make a frontend with html/css + js and also learn how to use web3, with that knowledge, im sure you can return to this project and make a very decent dapp for it, does not have to be pretty, just start learning how to built fast dapps :nerd_face:

Overall congratulations! you have made a nice work! there is always plenty of room for improvements, but this one is very nice!

Carlos Z

1 Like