Project - DEX Final Code

Thank you @ mcgrane5. This means really a lot to me. Very nice that you like the work. Keep going with your version. Can’t wait to see it.

2 Likes

I fully got all my contracts and migrations file correctly but I got lost on the test files …that I actually re write from @filip yet only 2 passes and others failed…I’ll share all my codes here pls help me check through and show me my errors

DEX.sol
` //SPDX-License: MIT
pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;
import “./DexWallet.sol”;

contract Dex is DexWallet {

//using SafeMath for uint256;

enum Side{ BUY, SELL}
//mapping(bytes32 => mapping(uint => order[]));
struct Order {
uint id;
address trader;
Side side;
bytes32 ticker;
uint amount;
uint price;
uint filled;
}

uint public nextOrderId = 0;

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

function getOrderBook(bytes32 ticker, Side side) view public returns(Order[] memory) {
return orderBook[ticker][uint(side)];
}
function createLimitOrder( Side side, bytes32 ticker, uint amount, uint price) public {
if(side == Side.BUY) {
require(balance[msg.sender][“ETH”] >= amount * price);
}
else if(side == Side.SELL) {
require(balance[msg.sender][“ticker”] >= amount);
}
Order[] storage orders = orderBook[ticker][uint(side)];
orders.push(Order(nextOrderId, msg.sender, side, ticker, amount, price));

//Bubble sort

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

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

}

function createMarketOrder( Side side, bytes32 ticker, uint amount) public {
if(side == Side.SELL){
require(balances[msg.sender[ticker] >= amount, “insufficient balance”);
}
// 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];
     cost storage = orders[i] * price;
uint totalfilled = 0;
for (uint256 i = 0; i < orders.length && totalfilled < amount; i++){
    //how much we can fill from order [1]
    uint leftToFilled = amount - totalfilled;
     uint availableToFill = orders[i].amount - orders[i].filled;
     uint filled =0;
     if(availableToFill > leftToFilled) {
         filled = leftToFilled;
     }
     else{
         filled = availableToFill;
     }
          
        totalfilled = totalfilled + filled;
    //update totalfilled
     if(side = Side.BUY){
        require(balances[msg.sender]["ETH"] >= filled * orders[i].price);
        //transfer eth from buyer to seller
        balances[msg.sender][ticker] = balances[msg.sender][ticker] + filled;
         balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"] - cost;
        // send tokens from seller to buy
        // send tokens from seller to buyer

        balances[orders[i].trader][ticker] = balances[orders[i].trader][ticker] - filled;
        balances[orders[i].trader]["ETH"] = balances[orders[i].trader]["ETH"] + cost;

     }
    //excute trade and shift balances between buyer and seller
      if(side = Side.SELL){
           balances[msg.sender][ticker] = balances[msg.sender][ticker] - filled;
         balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"] + cost;
        // send tokens from seller to buy
        // send tokens from seller to buyer

        balances[orders[i].trader][ticker] = balances[orders[i].trader][ticker] + filled;
        balances[orders[i].trader]["ETH"] = balances[orders[i].trader]["ETH"] -  cost;

      }
    // verify that the buyer has enough eth to cover the trade
}

}
//loop through the orderbook and remove 100% orderfilled
while( orders.length > 0 && orders[0].filled = orders[0].amount && orders.length -1 > 0) {
//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; i++) {
orders[i] = orders[i + 1];
}
orders.pop();
}
}

DexWallet.sol

pragma solidity  ^0.8.0;
import "../node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol";
//import "../node_modules/@openzeppelin/contracts/utils/math/Math.sol";
import "../node_modules/@openzeppelin/contracts/access/Ownable.sol";

contract DexWallet is Ownable {
//using SafeMath for uint256;

 // the struct is used to get the full details of the coins 
   struct Token {
    bytes32 ticker;
    address tokenAddress;
     }
mapping(bytes32 => Token) public tokenMapping;
bytes32[] public tokenList;


// the double mapping is used to track the muliple addresses that are using the exchange
mapping(address => mapping(bytes32 => uint256)) public balance;


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

function deposit(uint amount,bytes32 ticker) external {

}

function withdraw(uint amount, bytes32 ticker) external {
    require(tokenMapping[ticker].tokenAddress != address(0));
    require(balance[msg.sender][ticker] >= amount, "Balance Exceeded");
    balance[msg.sender][ticker] = balance[msg.sender][ticker] - amount;
    IERC20(tokenMapping[ticker].tokenAddress).transfer(msg.sender, amount);
}
    
}

Token.sol

pragma solidity  ^0.8.0;
import "../node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol";

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

}

Dex Migration

const Migrations = artifacts.require("Dex");
// const Dex = artifacts.require("Dex")
module.exports = function (deployer) {
  deployer.deploy(Migrations);
};

Dexwallet migration

const  DEXWallet = artifacts.require("DexWallet");

module.exports = function (deployer) {
  deployer.deploy(DEXWallet);
};

Token Migration

const Link = artifacts.require("Link");



module.exports =  function (deployer) {
   deployer.deploy(Link);
/* let DexWallet = await DexWallet.deployed()
 let Link = await Link.deployed()
 await Link.approve(DexWallet.address, 500)
DexWallet.addToken(web3.utils.fromUtf8("LINK"))
await DexWallet.deposit(100, web3.utils.fromUtf8("LINK"))
let balanceofLink = await DexWallet.balance(accounts[0], web3.utils.fromUtf8("LINK"))
console.log(balanceofLink);*/
};

Test Files:

Dextest.js

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

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

DexWalletest.js

const  Dex = artifacts.require("Dex")
const Link = artifacts.require("LINK")
const truffleAssert = 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 deposit 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.balance(accounts[0], web3.utils.fromUtf8("LINK"));

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

/*await deployer.deploy(Link);
 let DexWallet = await DexWallet.deployed()
 let Link = await Link.deployed()
 await Link.approve(DexWallet.address, 500)
DexWallet.addToken(web3.utils.fromUtf8("LINK"))
*/

})

Marketordertest.js

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

contract("Dex", accounts => {
 // when creating a SELL marketb order, the seller must have enough tokens for the trade
  it("should throw an error when creating a sell  market other without adequate toekn balance", async () => {

    let dex = await Dex.deployed()

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

            await truffleAssert.reverts(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 throw an error when creating a BUY market order without adequate balance", async () => {
    let dex = await Dex.deployed()
     
    let balance = await dex.balance(accounts[0], web3.utils.fromUtf8("ETH"))
    assert.equal(balance.toNumber(), 0, "initial ETH balance is not 0");

    await truffleAssert.reverts(dex.createMarketOrder(0, 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 order should be filled util the order book is emptied or the market order is 100% filled
it("market order should be filled util the order book is emptied or the market order is 100% filled", 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 token to accounts 1,2,3 from account o
 await link.transfer([1], 150)
 await link.transfer([2], 150)
 await link.transfer([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
await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 5, 300, {from: accounts[1]})
await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 5, 300, {from: accounts[2]})
await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 5, 300, {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 orderbook should have 0 filled");

})

//market orders should be filled untill the order book is empty or the market is 100% filled
it("market orders should be filled untill the order book is empty or the market is 100% filled", 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 again
      await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 5, 400, {from: accounts[1]})
      await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 5, 400, {from: accounts[2]})
       
      //check buyer link balance before link purchase
      let balanceBefore = await dex.balances(accounts[0], web3.utils.fromUtf8("LINK"))

      //create market order that could fill more than the entire order book(15 link)
      await dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 50);
       
      //check  buyer link balance after purchase
      let balanceAfter = await dex.balance(accounts[0], web3.utils.fromUtf8("LINK"))
     
      //buyer should have 15 more link aafter, even though order was for 50
      assert.equal(balance.toNumber() + 15, balance.toNumber());
})

   //the eth balance of the buyer should decrese with the filled amount
   it("the ethh balance should decrease with the filled amount", async () => {
       let dex = await Dex.deployed()
       let link = await Link.deployed()
        
       //seller depsoits link and creates  a sell limit order for 1 link for 300wei 
       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 thr limit order sellers should decrease with the amount filled
    it("the token balances of the limit ordee sellers should decrease with the filled amount", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed()
        let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1); //get sell side
        assert(orderbook.length == 0, "sell side orderbook should be empty at start of test");

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

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

        //check sellers link balance 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 order should be removed from the orderbook
    it("filled limit order shpuld be reomved from the orderbook", async () => {
        let dex = await Dex.deployed()
        let link = await Link.deployed
        await dex.addToken(web3.utils.fromUtf8("LINK"));
        await dex.depositEth({value: 10000});
        let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1); //get sell sode of the order book
        await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 1, 300)
        await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 1);
         orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1); // get sell side orderbook
         assert(orderbook.length == 0, "Sell side oder should be empty after trade"); 
        
    })

    //partly filled limit orders should be modified to represent the filled orremaining 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 order book should e emptied at the start of test");

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

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

    })

    // when creating  BUY market order the buyer needs to have enough ETH for the trade   
     it("should throw an error when creating a buy limit 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 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]}))
     })






})

these are all the files ols help me check don’t mind my too many files…I’m a learner…but I think the first error I was having is that my balance keep saying 0 instead of 100 eth…but pls help me check…thanks

1 Like

hey @Phaxsam, i can have a look at this later this evening for you but in the meantime what you should do is to (i know i always say this lol) is to do all of the tests manually in the truffle develop console. I.e deploy add token, approve token, deposit token, then create few limit orders, console.log the orderbook and see if they are indeed ordered correctly on both sides. Then send some ether and link to say accounts[1] and accounts[2] and create a few market orders and console.log the balances to see if they are correct also. So do all of this maually in the truffle console. If doing this works and all your functions behave as exoected then the problem lies withtin the syntax you are using or issues within your test files themselves. Again i will look into this for you this eveing when i get a chance.

Evan

1 Like

thanks Evan…I’ll be expecting your help…pls can I dm you?

1 Like

hey @Phaxsam. of course. yeah cool go for it

1 Like

Here is my dex final code. I work with a group of people in a study group. So my code has lots of comments and notes. This was very challenging as I created a code, based on the concepts that I learned. My code splits the sell and buy orders into groups.

  • David N
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0 < 0.9.0;
pragma experimental ABIEncoderV2;
import "../contracts/wallet.sol";

contract Dex is Wallet {

    using SafeMath for uint256;

    enum Category  {
        BUY,    // BUY = 0
        SELL    // SELL = 1
    }

    struct Order  {
        uint256 id;
        address trader;
        bytes32 ticker;
        Category orderType; 
        uint256 amount;
        uint256 price;
        uint256 filled;    // this variable will always start at 0 when an order is created
    }


    uint256 public transactionID = 0;  //this variable needs to be global.  If it's set inside a function, it would reset each time the function is called
 
    mapping(bytes32 => mapping(uint256 => Order[])) public orderBook;  // double mapping that points to an array of objects
    
    // function to get orderBook; second parameter will determine if 'buy orderBook' or 'sell orderBook' is retrieved
    function getOrderBook(bytes32 ticker, Category orderType) public view returns (Order[] memory)  {
        return orderBook[ticker][uint256(orderType)];
    }

    function createLimitOrder(bytes32 ticker, Category orderType, uint amount, uint price) public {
        uint256 filled = 0;    // filled must be 0 whenever a limit order is created as nothing is filled yet;
        // create buy limit order first
        if(orderType == Category.BUY)  {
            require(balances[msg.sender]["ETH"] >= amount.mul(price), "You don't have enough ETH for this buy order!"); //assume price means amount of ETH per ticker token
            Order[] storage buys = orderBook[ticker][uint(orderType)];  // stores current buy orderBook to this storage 'buys' array
            buys.push(Order(transactionID, msg.sender, ticker, orderType, amount, price, filled));

         // initiate bubbleSort for 'buy orderBook' array whenever buy limit order created
            uint256 i = buys.length > 0 ? buys.length - 1 : 0;
            // the array must have a minimum of 2 objects for the bubbleSort to run
            for(i; i > 0; i--)  {
                if(buys[i - 1].price < buys[i].price)  {    // only compares the price of the 2 objects in the array
                    Order memory moveBuy = buys[i - 1];     // temporary variable to save object
                    buys[i - 1] = buys[i];                  // must move entire object to the smaller index position, not just the price
                    buys[i] = moveBuy;                      // place former object into latter index position
                }                            
            }       
        }

        // create sell limit order
        else if(orderType == Category.SELL)  {
            require(balances[msg.sender][ticker] >= amount, "You don't have enough tokens for this sell order!");
            Order[] storage sells = orderBook[ticker][uint256(orderType)]; // stores current sell orderBook to this storage 'sells' array
            sells.push(Order(transactionID, msg.sender, ticker, orderType, amount, price, filled));

            // initiate bubbleSort for 'sell orderBook' array
            uint256 j = sells.length > 0 ? sells.length - 1 : 0;
            // the array must have a minimum of 2 objects for the bubbleSort to run
            for(j; j > 0; j--)  {
                if(sells[j - 1].price > sells[j].price)  {  // only compares the price of the 2 objects in the array
                    Order memory moveSell = sells[j - 1];   // temporary variable to save object
                    sells[j - 1] = sells[j];                // must move entire object to the smaller index position, not just the price
                    sells[j] = moveSell;                    // place former object into the latter index position
                }
            } 
        }

        transactionID++;    // must increment the ID inside the function but outside the code of the order creation so the ID is available for future buy or sell orders
    }



    function createMarketOrder(bytes32 ticker, Category orderType, uint amount) public {
        uint256 totalFilled = 0;  // to track filling of order
        uint256 toFill = amount;  // must create local variable to track changes to the amount as it's being filled

        if(orderType == Category.BUY)  {

            Order[] storage sells = orderBook[ticker][1];           // stores current sell orderBook in array
            uint256 balanceTracker = balances[msg.sender]["ETH"];   // saves ETH balance before any accounting changes
            uint256 buyCost = 0;     // variable to keep track of all buy costs & will use an assert statement at the end to see if buyer has enough ETH to execute

            for(uint256 i = 0; i < sells.length && amount > totalFilled; i++)  {   // the loop terminates when either it goes through all orders in the limit array or when amount of the limit order is filled

                // when 'toFill' amount is greater than the quantity of token in the current index position so the operation continues to the next index position
                if(toFill > sells[i].amount)  {
                    sells[i].filled = sells[i].filled.add(sells[i].amount); // will use the amount in that index's limit order for making accounting changes
                    totalFilled = totalFilled.add(sells[i].amount);

                    balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].sub(sells[i].amount.mul(sells[i].price));             // update ETH balance of buyer (subtract)
                    balances[sells[i].trader]["ETH"] = balances[sells[i].trader]["ETH"].add(sells[i].amount.mul(sells[i].price));   // update ETH balance of seller (add)

                    balances[sells[i].trader][ticker] = balances[sells[i].trader][ticker].sub(sells[i].amount);     // update token balance of seller (subtract)
                    balances[msg.sender][ticker] = balances[msg.sender][ticker].add(sells[i].amount);               // update token balance of buyer (add)

                    toFill = toFill.sub(sells[i].amount);     // update 'toFill' amount (this is the remaining qty of buy market order that still needs to be fulfilled)
                    buyCost = buyCost.add(sells[i].amount.mul(sells[i].price));  // update buy cost

                } else { 

                 // when 'toFill' amount is less than the quantity of token in the current index position; the market order will be completely filled in this loop  
                    sells[i].filled = sells[i].filled.add(toFill);     // will use the 'toFill' amount for making accounting changes 
                    totalFilled = totalFilled.add(toFill);

                    balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].sub(sells[i].amount.mul(sells[i].price));    // update ETH balance of buyer (subtract)
                    balances[sells[i].trader]["ETH"] = balances[sells[i].trader]["ETH"].add(toFill.mul(sells[i].price));   // update ETH balance of seller (add)

                    balances[sells[i].trader][ticker] = balances[sells[i].trader][ticker].sub(toFill);     // update token balance of seller (subtract)
                    balances[msg.sender][ticker] = balances[msg.sender][ticker].add(toFill);               // update token balance of buyer (add)

                    buyCost = buyCost.add(toFill.mul(sells[i].price));  // update buy cost
                }
                assert(balanceTracker >= buyCost); // checks if buyer had enough ETH, otherwise, it will revert the entire buy market order
            }  
            while(sells.length > 0 && sells[0].amount == sells[0].filled)  {  // only proceeds if there is at least one sell limit order and its index 0 position has been completely filled in the orderbook

                //when array has more than 1 object, it move it to end of the array
                if(sells.length > 1)  {
                    for(uint256 buyCounter = 0; buyCounter < sells.length - 1; buyCounter++) {  
                        Order memory moveSell = sells[buyCounter];
                        sells[buyCounter] = sells[buyCounter + 1];
                        sells[buyCounter + 1] = moveSell;
                        }
                    }
                sells.pop();   // removes the filled order at the end of the array      
            }   
            
        } else if(orderType == Category.SELL)   {
            require(balances[msg.sender][ticker] >= amount, "You are trying to sell more tokens than you own!");     
            Order[] storage buys = orderBook[ticker][0];  // stores current buy orderBook in array
            
            for(uint256 i = 0; i < buys.length && amount > totalFilled; i++)  {

                // when 'toFill' amount is greater than the quantity of token in the current index position so the operation will continue to next index position
                if(toFill > buys[i].amount)  {
                    buys[i].filled = buys[i].filled.add(buys[i].amount);    // will use the amount in that index's limit order for making accounting changes
                    totalFilled = totalFilled.add(buys[i].amount);

                    balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(buys[i].amount);            // update token balance of seller (subtract)
                    balances[buys[i].trader][ticker] = balances[buys[i].trader][ticker].add(buys[i].amount);    // update token balance of buyer (add)    

                    balances[buys[i].trader]["ETH"] = balances[buys[i].trader]["ETH"].sub(buys[i].amount.mul(buys[i].price));   //update ETH balance of buyer (subtract)
                    balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].add(buys[i].amount.mul(buys[i].price));           //update ETH balance of seller (add)

                    toFill = toFill.sub(buys[i].amount);      // update 'toFill' amount (this is the remaining qty of sell market order that still needs to be fulfilled)

                } else  {

                    // when 'toFill' amount is less than the quantity of token in the current index position; the market order will be completely filled in this loop        
                    buys[i].filled = buys[i].filled.add(toFill);       // will use the 'toFill' amount in the market order for making accounting changes
                    totalFilled = totalFilled.add(toFill);

                    balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(toFill);           // update token balance of seller (subtract)
                    balances[buys[i].trader][ticker] = balances[buys[i].trader][ticker].add(toFill);   // update token balance of buyer (add)

                    balances[buys[i].trader]["ETH"] = balances[buys[i].trader]["ETH"].sub(toFill.mul(buys[i].price));  // update ETH balance of buyer (subtract)
                    balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].add(toFill.mul(buys[i].price));          // update ETH balance of seller (add)
                }

            }

            while (buys.length > 0 && buys[0].amount == buys[0].filled)  {  // only proceeds if there is at least one sell limit order and its index 0 position has been completely filled in the orderbook
                // when array has more than 1 object
               if(buys.length > 1)  {
                    for(uint256 moveCounter = 0; moveCounter < buys.length - 1; moveCounter++)  {
                        Order memory moveBuy = buys[moveCounter];
                        buys[moveCounter] = buys[moveCounter + 1];
                        buys[moveCounter + 1] = moveBuy;
                    }
                } 
                buys.pop();  // removes the filled order at the end of the array
            }  
        }
    }
}

1 Like

Hello,

Here is my code again. I fixed a bug.

// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0 < 0.9.0;
pragma experimental ABIEncoderV2;
import "../contracts/wallet.sol";

contract Dex is Wallet {

    using SafeMath for uint256;

    enum Category  {
        BUY,    // BUY = 0
        SELL    // SELL = 1
    }

    struct Order  {
        uint256 id;
        address trader;
        bytes32 ticker;
        Category orderType; 
        uint256 amount;
        uint256 price;
        uint256 filled;    // this variable will always start at 0 when an order is created
    }


    uint256 public transactionID = 0;  //this variable needs to be global.  If it's set inside a function, it would reset each time the function is called
 
    mapping(bytes32 => mapping(uint256 => Order[])) public orderBook;  // double mapping that points to an array of objects
    
    // function to get orderBook; second parameter will determine if 'buy orderBook' or 'sell orderBook' is retrieved
    function getOrderBook(bytes32 ticker, Category orderType) public view returns (Order[] memory)  {
        return orderBook[ticker][uint256(orderType)];
    }

    function createLimitOrder(bytes32 ticker, Category orderType, uint amount, uint price) public {
        uint256 filled = 0;    // filled must be 0 whenever a limit order is created as nothing is filled yet;
        // create buy limit order first
        if(orderType == Category.BUY)  {
            require(balances[msg.sender]["ETH"] >= amount.mul(price), "You don't have enough ETH for this buy order!"); //assume price means amount of ETH per ticker token
            Order[] storage buys = orderBook[ticker][uint(orderType)];  // stores current buy orderBook to this storage 'buys' array
            buys.push(Order(transactionID, msg.sender, ticker, orderType, amount, price, filled));

         // initiate bubbleSort for 'buy orderBook' array whenever buy limit order created
            uint256 i = buys.length > 0 ? buys.length - 1 : 0;
            // the array must have a minimum of 2 objects for the bubbleSort to run
            for(i; i > 0; i--)  {
                if(buys[i - 1].price < buys[i].price)  {    // only compares the price of the 2 objects in the array
                    Order memory moveBuy = buys[i - 1];     // temporary variable to save object
                    buys[i - 1] = buys[i];                  // must move entire object to the smaller index position, not just the price
                    buys[i] = moveBuy;                      // place former object into latter index position
                }                            
            }       
        }

        // create sell limit order
        else if(orderType == Category.SELL)  {
            require(balances[msg.sender][ticker] >= amount, "You don't have enough tokens for this sell order!");
            Order[] storage sells = orderBook[ticker][uint256(orderType)]; // stores current sell orderBook to this storage 'sells' array
            sells.push(Order(transactionID, msg.sender, ticker, orderType, amount, price, filled));

            // initiate bubbleSort for 'sell orderBook' array
            uint256 j = sells.length > 0 ? sells.length - 1 : 0;
            // the array must have a minimum of 2 objects for the bubbleSort to run
            for(j; j > 0; j--)  {
                if(sells[j - 1].price > sells[j].price)  {  // only compares the price of the 2 objects in the array
                    Order memory moveSell = sells[j - 1];   // temporary variable to save object
                    sells[j - 1] = sells[j];                // must move entire object to the smaller index position, not just the price
                    sells[j] = moveSell;                    // place former object into the latter index position
                }
            } 
        }

        transactionID++;    // must increment the ID inside the function but outside the code of the order creation so the ID is available for future buy or sell orders
    }



    function createMarketOrder(bytes32 ticker, Category orderType, uint amount) public {
        uint256 totalFilled = 0;  // to track filling of order
        uint256 toFill = amount;  // must create local variable to track changes to the amount as it's being filled

        if(orderType == Category.BUY)  {

            Order[] storage sells = orderBook[ticker][1];           // stores current sell orderBook in array
            uint256 balanceTracker = balances[msg.sender]["ETH"];   // saves ETH balance before any accounting changes
            uint256 buyCost = 0;     // variable to keep track of all buy costs & will use an assert statement at the end to see if buyer has enough ETH to execute

            for(uint256 i = 0; i < sells.length && amount > totalFilled; i++)  {   // the loop terminates when either it goes through all orders in the limit array or when amount of the limit order is filled

                // when 'toFill' amount is greater than the quantity of token in the current index position so the operation continues to the next index position
                if(toFill > sells[i].amount)  {
                    sells[i].filled = sells[i].filled.add(sells[i].amount); // will use the amount in that index's limit order for making accounting changes
                    totalFilled = totalFilled.add(sells[i].amount);

                    balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].sub(sells[i].amount.mul(sells[i].price));             // update ETH balance of buyer (subtract)
                    balances[sells[i].trader]["ETH"] = balances[sells[i].trader]["ETH"].add(sells[i].amount.mul(sells[i].price));   // update ETH balance of seller (add)

                    balances[sells[i].trader][ticker] = balances[sells[i].trader][ticker].sub(sells[i].amount);     // update token balance of seller (subtract)
                    balances[msg.sender][ticker] = balances[msg.sender][ticker].add(sells[i].amount);               // update token balance of buyer (add)

                    toFill = toFill.sub(sells[i].amount);     // update 'toFill' amount (this is the remaining qty of buy market order that still needs to be fulfilled)
                    buyCost = buyCost.add(sells[i].amount.mul(sells[i].price));  // update buy cost

                } else { 

                 // when 'toFill' amount is less than the quantity of token in the current index position; the market order will be completely filled in this loop  
                    sells[i].filled = sells[i].filled.add(toFill);     // will use the 'toFill' amount for making accounting changes 
                    totalFilled = totalFilled.add(toFill);

                    balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].sub(toFill.mul(sells[i].price));    // update ETH balance of buyer (subtract)
                    balances[sells[i].trader]["ETH"] = balances[sells[i].trader]["ETH"].add(toFill.mul(sells[i].price));   // update ETH balance of seller (add)

                    balances[sells[i].trader][ticker] = balances[sells[i].trader][ticker].sub(toFill);     // update token balance of seller (subtract)
                    balances[msg.sender][ticker] = balances[msg.sender][ticker].add(toFill);               // update token balance of buyer (add)

                    buyCost = buyCost.add(toFill.mul(sells[i].price));  // update buy cost
                }
                assert(balanceTracker >= buyCost); // checks if buyer had enough ETH, otherwise, it will revert the entire buy market order
            }  
            while(sells.length > 0 && sells[0].amount == sells[0].filled)  {  // only proceeds if there is at least one sell limit order and its index 0 position has been completely filled in the orderbook

                //when array has more than 1 object, it move it to end of the array
                if(sells.length > 1)  {
                    for(uint256 buyCounter = 0; buyCounter < sells.length - 1; buyCounter++) {  
                        Order memory moveSell = sells[buyCounter];
                        sells[buyCounter] = sells[buyCounter + 1];
                        sells[buyCounter + 1] = moveSell;
                        }
                    }
                sells.pop();   // removes the filled order at the end of the array      
            }   
            
        } else if(orderType == Category.SELL)   {
            require(balances[msg.sender][ticker] >= amount, "You are trying to sell more tokens than you own!");     
            Order[] storage buys = orderBook[ticker][0];  // stores current buy orderBook in array
            
            for(uint256 i = 0; i < buys.length && amount > totalFilled; i++)  {

                // when 'toFill' amount is greater than the quantity of token in the current index position so the operation will continue to next index position
                if(toFill > buys[i].amount)  {
                    buys[i].filled = buys[i].filled.add(buys[i].amount);    // will use the amount in that index's limit order for making accounting changes
                    totalFilled = totalFilled.add(buys[i].amount);

                    balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(buys[i].amount);            // update token balance of seller (subtract)
                    balances[buys[i].trader][ticker] = balances[buys[i].trader][ticker].add(buys[i].amount);    // update token balance of buyer (add)    

                    balances[buys[i].trader]["ETH"] = balances[buys[i].trader]["ETH"].sub(buys[i].amount.mul(buys[i].price));   //update ETH balance of buyer (subtract)
                    balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].add(buys[i].amount.mul(buys[i].price));           //update ETH balance of seller (add)

                    toFill = toFill.sub(buys[i].amount);      // update 'toFill' amount (this is the remaining qty of sell market order that still needs to be fulfilled)

                } else  {

                    // when 'toFill' amount is less than the quantity of token in the current index position; the market order will be completely filled in this loop        
                    buys[i].filled = buys[i].filled.add(toFill);       // will use the 'toFill' amount in the market order for making accounting changes
                    totalFilled = totalFilled.add(toFill);

                    balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(toFill);           // update token balance of seller (subtract)
                    balances[buys[i].trader][ticker] = balances[buys[i].trader][ticker].add(toFill);   // update token balance of buyer (add)

                    balances[buys[i].trader]["ETH"] = balances[buys[i].trader]["ETH"].sub(toFill.mul(buys[i].price));  // update ETH balance of buyer (subtract)
                    balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].add(toFill.mul(buys[i].price));          // update ETH balance of seller (add)
                }

            }

            while (buys.length > 0 && buys[0].amount == buys[0].filled)  {  // only proceeds if there is at least one sell limit order and its index 0 position has been completely filled in the orderbook
                // when array has more than 1 object
               if(buys.length > 1)  {
                    for(uint256 moveCounter = 0; moveCounter < buys.length - 1; moveCounter++)  {
                        Order memory moveBuy = buys[moveCounter];
                        buys[moveCounter] = buys[moveCounter + 1];
                        buys[moveCounter + 1] = moveBuy;
                    }
                } 
                buys.pop();  // removes the filled order at the end of the array
            }  
        }
    }
}

1 Like

Thanks man, appreciated! :slight_smile:

1 Like

Hello @thecil and @filip, since the new website I have a few issues. Filip mentioned that he will suggest a few things to improve the DEX. Do you know what are those improvements so I can start working on them? Thank you, Vilior

Hey @vili, hope you are ok.

Would you please link me the lesson of the course? :nerd_face:

Maybe we forgot to add that content

Carlos Z

2 Likes

Hey @thecil its the last video in the 201 course of the dex project section the video just after filip reviews the final code.

1 Like

thanks @mcgrane5, your always helping us, i do appreciate it man :nerd_face:

I will discuss it with the team and we will have some new improvements to challenge you guys :muscle:

Carlos Z

1 Like

Hey guys,

Here is my final code on Github: https://github.com/Pedrojok01/Dex_With_OrderBook

This new Ethereum 201 course felt much much easier than the old one! Maybe because we didn’t had to deal with the front-end/web3… Or maybe I’m just getting better at coding :sweat_smile:!

Anyway, any feedback is welcomed. I’ll try to work on a React front-end next, and maybe add a few more tokens and functions to make it more “real”.

For the order-book sorting, too bad there is no .shift method in solidity :joy:
Thanks for the course @filip.

Pierre.

1 Like

Hey @Pedrojok01. I thought so too its probably becauae filip does a complete walkthrough in this one but i definitely did enjoy the other one more felt more rewarding. and its actually quite strange for people only joining the old 201 course is not available anymore on the new academy website. Not sure if this is done on purpose or if it forgot to be added.

@dan-i have you any insight on this. I actually messaged carloz yesterday about thia but he has not seen my message yet

1 Like

You can check out my solution here:
raphbaph/DEX (github.com)

I used another approach to filling, where I simply adjust the amounts of the order and don’t introduce a new variable.

One thing I noticed missing in @filip’s solution:

Limit orders are never actually filled.
Unfilled or partially filled market orders are never stored.

I still have the same issues in my code and will try to fix it asap.

2 Likes

Hey @raphbaph y eah this is a limitation to filips code. What i did in my code was i made a seperate function called settleOrder() which i call at the top of both the market order function and limit order function. This way bedore you create any type of order the settle Order function is called which resolves the orderbook if needs be before adding new ones.

What you could also do to fix the problem of limit orders not getting settled is to change your code so that if a limit order is submitted and there is one on the opposite side with the exact same price then you run the logic to settle the orders you could do a simpleif statement check to accomplish this.

So to explain more on how to make a settleOrder function just remove the logic from the market order function and place it into its own “private” function then just call this function in the place of where the old code used to be in the market order func. You may have to do your own tweaking of the code to get this to work. Personally i didnt really lile filips solution if you want to use it its works great but i provided a psudeoCode in a post up above of the algorithm i made for my settleOrder function give it a look if you want to implement these changes and are curious. Note that its very much a psudeo code and may be hard to follow and also does not capture the complete functionality of my final settleIrder function but it does give a general outline

Edit my bad that post is in the market order test forum not this one sorry. Anyways you should deffo try your own approach here to make your great code even better. it will be very rewarding

Evan

2 Likes

Hi @mcgrane5!
Thanks for your thoughtful answer!
settleOrder() is definitely needed, will implement it now.
However, that leaves the unfulfilled market orders to be stored.
They would actually have to be at the top of the order book for as long as they’re filled.
Since they accept any price on the other side.
So one way to do that is to insert them at the [0] position of the respective Order array and probably leave the price empty.
The settleOrder () function could then fill that.
How did you handle that?

1 Like

@mcgrane5 Yeah, also maybe because of the new kitties Dapp course. Haven’t done it yet, but on my list. Must say that the old ETH 201 was quite outdated (softwares dependencies/version). Sometimes, the hardest was to get a running config haha!

@raphbaph Do we really need to store the market orders though? I think we could just retrieve the needed info from an event, like in case the user wants to check his history). That would save a lot on storage in the long run. Or am I missing something?

1 Like

@raphbaph in my code i made the ability to store market orders. For example if the orderbook is empty or if a market order does not get fully filled from a limit order then its remaining amount will be availiable in the orderbook. You can look at my code to see my approach to the settleOrder() if your maybe you will get an idea or two for yours. I am quite happy with my solution looking back. but note that i completed this course nearly 2 months ago i have already grown far more in soldiity and programming in general, since then since i have pretty much been coding flat out this summer so there are many things i would change i dont think my solution is perfect or as good a one as i could make now also in that i think my method of the settling of limit orders of tis not fully perfect but when i make my front end for this project i will be revisiting my algorithm as i know now how to fix it to be able to efficiently both settle market orders and limit orders on the opposite side with same price for the various edge cases. just letting you know incase you do wish to take a loot at my code for some ideas because its a different approach to filips solutions from the videos

https://github.com/mcgraneder/SimpleDecentralisedExchange

2 Likes

Thanks for sharing your code. I’d definitely store market orders too.
Realistically, we’d need to use Oracles and store and process the order books off chain.
It’s just too expensive the way it is done now, and too slow.
Which other courses did you take?
I’m currently enrolled in Chainlink 101, which is really advanced, in a way :slight_smile:

2 Likes