Project - DEX Final Code

hey @raphbaph. so ive currentlt taken solidity 101 201, 201[OLD] and the ethereum game programming course. I have actually stopped some of the course here as i am taking some time to learn frontend development and backend development like databases and APIs and all to better myself as a programmer before starting my dex frontend because i want to include live price feeds and live charts for ethereum for example etc. after that i plan to do SC security and chainlink, dapp programming etc and the EOS courses too

2 Likes

Very cool! I don’t like to do front-end so much.
Will deep dive on Chainlink and try to run a Node myself.
After that I enrolled in the Consensys Bootcamp starting in September.

But will still do the dApp or Games course here at Ivan. Love the courses here.
So accessible.

2 Likes

Hello @dan-i, @filip

Am having issue using web3.am getting below error message

con.js:13 Uncaught ReferenceError: Web3 is not defined
    at con.js:13
1 Like

are you properly importing web3 into your js file and declsaring your window.web3 object properly?

No i did npm install web3 on the part i am doing my project

1 Like

I assume your working on some sort of front end. Not im not sure cos you have not provided ypur code. But you need to import web 3. Maybe this is why its not defined. So your initial main.js template could be something like

import Web3 from 'web3';

import youContractName from '../abis/YourContractName.json'

//this function is standard as per the metamask docs. you need to use this code to 
//allow your browser to connect to the blockchain (it does this trhough metamask)

init = async () => {
    loadWeb3()
    loadBlockchainData()
}

loadWeb3()  = async () => {
    if (window.ethereum) {
      window.web3 = new Web3(window.ethereum)
      await window.ethereum.enable()
      console.log("hello");
    }
    else if(window.web3) {
      window.web3 = new Web3(window.web3.currentProvider)
    }
    else {
      window.alert("non-Ethereum browser detected.");
    }
  }


loadBlockchainData()  = async () => {
    const web3 = window.web3
    // Load account
    const accounts = await web3.eth.getAccounts()
   
    // Network ID
    const networkId = await web3.eth.net.getId()
    const networkData = yourContractName.networks[networkId]
    if(networkData) {
      const contractInstance = new web3.eth.Contract(yourContractName.abi, networkData.address)
      
    }
    else {
      alert("cannot find contract address for detected network");
    }
   
  }

init();



Ok so im not sure at all if this will help you or not as its only my guess that you have not imported web3 and thats what the error is but without seeing your code i could be completely wrong. To initialise your js file to connect to eth blockhain using metaMask and web3.js the above template is not bad

it has two main functions. loadWeb3() and loadBlockchainData(). LoadWeb3 is responsible for connecting your browser to the ethereum blockchain through metamask. Without this function you will not be able to use web3.js. This is adapted from the metamask docs.

The loadBlockchainData function is responsible for connecting our smart contratcs backend to our frontend. It does so by creating an instance of ouf our smart contratcs so that we can access its functions to read and write to the blockchain (call and send Txs). This function also gets the current account we are using the dApp in. If you use this template you should not get that web3 error.

Also the imports at the top are only nessecary for dynamically fetching the contract abi and address for creating our contract instance. Hope this makes sense again not sure if this is a solution to your problem but if you are making a front end this template is good to use

2 Likes

Hey @mcgrane5 i tried the import statement i got below error

con.js:7 Uncaught SyntaxError: Cannot use import statement outside a module

I used type module on the html i got below

localhost/:1 Uncaught TypeError: Failed to resolve module specifier "web3". Relative references must start with either "/", "./", or "../".

Below is my code

con.js

//var web3 = new Web3(Web3.givenProvider);

//web3.eth.net.getId()
//ethereum.enable();

///Check if Metamask is installed
import Web3 from 'web3';
if(typeof window.ethereum !== 'undefined'){
    console.log('metaMast Is installed');
}else{
    console.log('metaMast Is not installed');
}

var web3 = new Web3(Web3.givenProvider);

///Variable declearation
const etherButton = document.querySelector('.enableEthereumButton');
const showAccount = document.querySelector('.showAccount');
const sendEth = document.querySelector('.sendEth');
const showBal = document.querySelector('.showBal');
const AproveContract = document.querySelector('.AproveContract');
var instance;
var user;
var contractAddress ="0xF250AB1bf7207F493cB9c6451AF32D554cC6433c";


etherButton.addEventListener('click',()=>{
    getAccounts();
});

AproveContract.addEventListener('click',()=>{
    ApproveCon();
});

async function ApproveCon(){
    console.log("Here");
    
    //instance =  new.web3.eth.Contract(abi,contractAddress, {from: ethereum.selectedAddress});
    console.log(instance);
    
}

///Called once account is been change on metamask
ethereum.on('accountsChanged', function (accounts) {
    // Time to reload your interface with accounts[0]!
    console.log("Network: " + ethereum.networkVersion);
    console.log("Selected: " + ethereum.selectedAddress);
    showAccount.innerHTML = ethereum.selectedAddress;
});

async function getAccounts() {
    console.log('In');

    const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
    const account = accounts[0];
    showAccount.innerHTML = account;
    const balance = await ethereum.request({
        method:'eth_getBalance',
        params: [account, "latest"]
    });

    const read = parseInt(balance) /10**18;
    console.log(read.toFixed(5));
    showBal.innerHTML = read.toFixed(5);

    console.log("Network: " + ethereum.networkVersion);
    console.log("Selected: " + ethereum.selectedAddress);
}

const ethereumButton = document.querySelector('.enableEthereumButton');
const sendEthButton = document.querySelector('.sendEthButton');

let accounts = [];

//Sending Ethereum to an address
sendEth.addEventListener('click', () => {
  ethereum
    .request({
      method: 'eth_sendTransaction',
      params: [
        {
          //from: accounts[0],
          from: ethereum.selectedAddress,
          to: '0x58B7D176ae545Ff8db3246b13e6c7f593f3e9E4b',
          value: '100000000000000',
          //gasPrice: '0x09184e72a000',
          //gas: '0x2710',
        },
      ],
    })
    .then((txHash) => console.log(txHash))
    .catch((error) => console.error);
});



/*const transactionParameters = {
    nonce: '0x00', // ignored by MetaMask
    gasPrice: '0x09184e72a000', // customizable by user during MetaMask confirmation.
    gas: '0x2710', // customizable by user during MetaMask confirmation.
    to: '0x0000000000000000000000000000000000000000', // Required except during contract publications.
    from: ethereum.selectedAddress, // must match user's active address.
    value: '0x00', // Only required to send ether to the recipient from the initiating external account.
    data:
      '0x7f7465737432000000000000000000000000000000000000000000000000000000600057', // Optional, but used for defining smart contract creation and interaction.
    chainId: '0x3', // Used to prevent transaction reuse across blockchains. Auto-filled by MetaMask.
  };
  
  // txHash is a hex string
  // As with any RPC call, it may throw an error
  const txHash = await ethereum.request({
    method: 'eth_sendTransaction',
    params: [transactionParameters],
  });*/

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Page Title</title>
</head>
<body>
    <button class="enableEthereumButton">Enable Ethereum</button>
    
    <h2>Account: <span class="showAccount"></span></h2>
    <button class="sendEth">Send Eth</button>
    <h2>Balance: <span class="showBal"></span></h2>
    <h2>Contract: <input id="AprCon" class="AprCon"/> </h2>
    <button class="AproveContract">ApproveCon</button>

</body>
<script src="ABI.js"></script>
<script type="module" src="con.js"></script>
</html>
1 Like

hey @chim4us, hmm i was just about to say try adding a module type to your con.js script in the html file as that seems to be a good fix just from doing a quick google there. However you have already done this. Ok lets try to add web3 without an import. You can also do it this way by

var web3 = new Web3(Web3.givenProvider);

in place of what used to be your import. And then you must also include we3.j sin your html file so include this script in your head section not the body like you currently have. Also just to be sure include you con.js file as a script in the head section also but change type to text/javascript

<script type="text/javascript" src="./web3.min.js"></script>
<script type="text/javascript" src="./con.js"></script>

This should work for you.

Here is my code below…passed all tests!

Question: I had to remove ‘onlyOwner’ from my create limit order function in order to run tests. Should I put that back in now that tests have passed? I assume only the owner should be able to post limit orders!

pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;

import "./wallet.sol";

contract Dex is Wallet {
    
    enum Side {
        BUY,
        SELL
    }

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

    uint public nextOrderId = 0;

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

    //same input getOrderBook(bytes32("LINK"),Side.BUY) 
    function getOrderBook(bytes32 ticker, Side side) view public returns(Order[] memory) {
        return orderBook[ticker][uint(side)];
    }
    
    //swapping elements in orderbook
    function _swap(Order[] storage _orders, uint p1, uint p2) private {
        Order memory first_order = _orders[p1];

        //swapping objects in array
        _orders[p1] = _orders[p2];
        _orders[p2] = first_order;
    }

    //create limit order to buy/sell ticker in the amount of 'amount' at 'price'
    function createLimitOrder(Side side, bytes32 ticker,uint amount, uint price) public tokenExist(ticker) {
        if (side == Side.BUY) {
            //check if users has enough ETH for buy order
            uint cost = amount * price; //in ether
            require(balances[msg.sender][bytes32("ETH")] >= cost,"Insufficient ETH Balance!");
        }
        else { //sell order
        
            //check if users has enough tokens to sell
            require(balances[msg.sender][ticker] >= amount, "Insufficient Token Balance!");

        }

        //creating pointer array to orders for ticker / side
        Order[] storage orders = orderBook[ticker][uint(side)];
        //[order1, order2]

        //adding new order to order book
        orders.push(Order(nextOrderId,msg.sender,side,ticker,amount,price));

        //Sorting order book, placing new order in right place
        if (side == Side.BUY){  //sorting highest to lowest price

            for (uint i = orders.length - 1;i > 0;i--){
                if (orders[i].price > orders[i-1].price) {
                    //orders unsorted, swapping elements
                    _swap(orders,i,i-1);
                }
                else break;
            }
        }
        else if (side == Side.SELL) {  //sorting lowest to highest price
            for (uint i = orders.length - 1;i > 0;i--){
                if (orders[i].price < orders[i-1].price) {
                    //orders unsorted, swapping elements
                    _swap(orders,i,i-1);
                }
                else break;
            }


        }
        nextOrderId++; 
    }

    //determine how many 'ticker' tokens in SELL or BUY book 
    function tokenAmount(Side side, bytes32 ticker) public view tokenExist(ticker) returns (uint) {
        //creating pointer array to orders for ticker / side
        Order[] storage _orders = orderBook[ticker][uint(side)];
        uint size = _orders.length;
        uint totalTokens = 0;

        //looping to shift elements n units over
        for (uint i=0;i < size;i++) totalTokens += _orders[i].amount;

        return totalTokens;
    }

    //determine total BUY or SELL orderbook value in wei
    function valueAmount(Side side, bytes32 ticker) public view tokenExist(ticker) returns (uint) {
        //creating pointer array to orders for ticker / side
        Order[] storage _orders = orderBook[ticker][uint(side)];
        uint size = _orders.length;
        uint totalValue = 0;

        //looping to shift elements n units over
        for (uint i=0;i < size;i++) totalValue += _orders[i].amount * _orders[i].price;

        return totalValue;
    }

    //remove first n orders from orderbook while keeping orderbook sorted
    function removeOrders(Side side, bytes32 ticker, uint n) private tokenExist(ticker) {
        //creating pointer array to orders for ticker / side
        Order[] storage _orders = orderBook[ticker][uint(side)];
        uint size = _orders.length;

        //looping to shift elements n units over
        for (uint i=n;i < size;i++){
            _orders[i-n] = _orders[i];
        }

        //popping last to pop last n elements from list (duplicates)
        for (uint i=0;i < n;i++) _orders.pop();

    }

    //execute an order between a buyer and a seller of 'amount' ticker at 'price'
    function executeOrder(address _buyer,address _seller, bytes32 tick, uint amount, uint price) private tokenExist(tick) {

        //making sure _seller as enough ticker and _buyer has enough ETH
        require(balances[_seller][tick] >= amount,"Insuffient seller token balance!");
        require(balances[_buyer][bytes32("ETH")] >= amount * price,"Insuffient buyer ETH!");

        //tranferring 'amount' of ticker tokens to _buyer's balance
        balances[_seller][tick] -= amount;
        balances[_buyer][tick] += amount;

        //tranferring proceeds ( 'amount * price ' ) of sale to seller account
        balances[_buyer][bytes32("ETH")] -= amount * price;
        balances[_seller][bytes32("ETH")] += amount * price;

    }
    
    /****************************************************************** */
    /****************************************************************** */
    //create and execute market orders
    /****************************************************************** */
    /****************************************************************** */
    function createMarketOrder(Side side, bytes32 ticker,uint amount) public tokenExist(ticker) onlyOwner returns (uint){

        //FIRST CHECK - if SELL make sure seller has enough Tokens
        if (side == Side.SELL) { 
                require(balances[msg.sender][ticker] >= amount,"Insufficient Token Balance!");
            }

        //if market order is SELL it will use BUY book, if market order is BUY it will use sell book
        Side orderbookSide;
        if (side == Side.SELL) orderbookSide = Side.BUY;
        else orderbookSide = Side.SELL;
        //creating pointer array to orders for ticker / side
        Order[] storage orders = orderBook[ticker][uint(orderbookSide)];
        //[order1, order2]

        //check is at least 1 order in order book, otherwise operation complete
        if (orders.length == 0) return 0;

        //calculating total token amount and total value of orderbook
        uint orderBookValue = valueAmount(orderbookSide, ticker);
        uint orderBookTokens = tokenAmount(orderbookSide, ticker);

        //counting remaining amount of ticker that needs to be sold or bought
        uint remaining = amount;

        //number of limit orders that needs to be COMPLETELY filled for this market order
        uint orderFillCount = 0; 
        bool partialFill = false; //true if there is a partial fill of an limit order
        uint orderValue = 0; //total order value based on limit order prices
     
        //checking if there are enough tokens to fulfill market order
        if (amount >= orderBookTokens){  //if true, need to empty out order book fullfill market order
            orderFillCount = orders.length;
            orderValue = orderBookValue;
            remaining = 0;
        }
        else {  //amount < orderBookTokens
            //looping through order book to determine how many limit orders will 
            //be need to be filled and calc total order value
            for (uint i = 0;i < orders.length;i++){

                //determine if order will be filled by orders[i].amount
                if (remaining > orders[i].amount) { //if true, current order is not enough to fill market order
                        orderFillCount++;
                        remaining -= orders[i].amount;
                        orderValue += orders[i].amount * orders[i].price;
                    }
                else if (remaining == orders[i].amount){ //exactly enough to fill order
                        orderFillCount++;
                        orderValue += orders[i].amount * orders[i].price;
                        remaining = 0;
                        break;
                    }
                else { // means remaining < order[i].amount => partial fill
                        partialFill = true;
                        orderValue += remaining * orders[i].price;
                        break;
                    }

            } // end for (uint i = 0;i < size;i++)
        } // close else for (amount > orderBookTokens)

        //THIRD CHECK - now that we know order value, make sure if market buy order, they have enough ETH
        if (side == Side.BUY) {
                //check if users has enough ETH for buy order
                require(balances[msg.sender][bytes32("ETH")] >= orderValue,"Insufficient ETH Balance!");
            }

        //looping through array to fulfill buy order
        if (side == Side.BUY) { 

            for (uint i = 0;i < orderFillCount;i++){
                    //transfering ETH to EACH seller & transfering 'ticker' to buyer for each trade
                    executeOrder(msg.sender,orders[i].trader,ticker,orders[i].amount,orders[i].price);
                }
            //if there is partial order to fill, fill it now
            if (partialFill){
                    //partial fill order will be at position +1 from where above loop ended 
                    executeOrder(msg.sender,orders[orderFillCount].trader,ticker,remaining,orders[orderFillCount].price);

                    //updating remaining balance for order[orderFillCount].trader in orderbook
                    orders[orderFillCount].amount -= remaining;
                
                }
            } // end if (side == Side.BUY)
        //looping through array to fulfill sell order    
        else if (side == Side.SELL) {
            for (uint i = 0;i < orderFillCount;i++){
                    //transfering ETH to seller & transfering 'ticker' to EACH buyer
                    executeOrder(orders[i].trader,msg.sender,ticker,orders[i].amount,orders[i].price);
                }
            //if there is partial order to fill, fill it now
            if (partialFill){
                    //partial fill order will be at position +1 from above loop
                    executeOrder(orders[orderFillCount].trader,msg.sender,ticker,remaining,orders[orderFillCount].price);

                    //updating remaining balance for order[orderFillCount].trader in orderbook
                    orders[orderFillCount].amount -= remaining;

                }  
            } // end  else if (side == Side.SELL)
        
        //remove and clean up order book of fully executed orders
        removeOrders(orderbookSide,ticker,orderFillCount);

        //returning ordervalue
        return orderValue;
    }
}

Here is my first test harness. code for tests was LONGER than the dex.sol code lol!

// BUY (ask): user's ETH > buy order value
// SELL (bid): user must have enough tokens such that token amount is greater than sell order amount
//the first order [0] in the BUY book must have highest price
//the last order in SELL book must have lowest price ().length - 1
const Dex = artifacts.require("Dex");
const Link = artifacts.require("Link");
const truffleAssert = require('truffle-assertions');

contract.skip("Dex",accounts => {
    it("should only be possible for owners to add tokens", async () => {
  
        //creating instance of wallet contract
        let dex = await Dex.deployed();
      
        //creating instance of LINK token contract
        let link = await Link.deployed();
        
        //adding token ticker and address to wallet
        //await link.approve(dex.address,2000);
        await truffleAssert.passes(dex.addToken(web3.utils.fromUtf8("LINK"),link.address, {from: accounts[0]}));

        //making sure token cannot be added twice
        await truffleAssert.reverts(dex.addToken(web3.utils.fromUtf8("LINK"),link.address, {from: accounts[0]}));
    })
    it("should handle deposits correctly", async () => {
  
        //creating instance of wallet contract
        let dex = await Dex.deployed();
      
        //creating instance of LINK token contract
        let link = await Link.deployed();

        //depositing 100 link into dex wallet
        await link.approve(dex.address,2000);
        await dex.deposit(2000,web3.utils.fromUtf8("LINK"));
        
        let balance = await dex.balances(accounts[0],web3.utils.fromUtf8("LINK"));

        //checking LINK balance is properly deposited
        assert.equal(balance.toNumber(),2000);
        
    })
    it("should be able to submit market order with nothing in order book", async () => {
  
        //creating instance of wallet contract
        let dex = await Dex.deployed();
      
        //creating instance of LINK token contract
        let link = await Link.deployed();

        //submitting market order
        await truffleAssert.passes(dex.createMarketOrder(Dex.Side.BUY,web3.utils.fromUtf8("LINK"),100));
        
    })
    it("user should have enough ETH for limit buy order in account", async () => {
  
        //creating instance of wallet contract
        let dex = await Dex.deployed();
      
        //creating instance of LINK token contract
        let link = await Link.deployed();

        //checking to make sure order with no ETH does not go through
        await truffleAssert.reverts(dex.createLimitOrder(Dex.Side.BUY,web3.utils.fromUtf8("LINK"),100,2));
       
        //depositing ETH
        dex.depositEth({value: 100000},{from: accounts[0]});

        //checking to make sure order now passes
        await truffleAssert.passes(dex.createLimitOrder(Dex.Side.BUY,web3.utils.fromUtf8("LINK"),100,200));
       
    })
    it("user should have enough Tokens for limit sell order in account", async () => {
  
        //creating instance of wallet contract
        let dex = await Dex.deployed();
      
        //creating instance of LINK token contract
        let link = await Link.deployed();

        //checking to make sure too large order fails, current balance is 2000 LINK (see above)
        await truffleAssert.reverts(dex.createLimitOrder(Dex.Side.SELL,web3.utils.fromUtf8("LINK"),20000,2));
       
        //checking to make sure smaller order passes, current balance is 2000 LINK (see above)
        await truffleAssert.passes(dex.createLimitOrder(Dex.Side.SELL,web3.utils.fromUtf8("LINK"),100,200));
       
    })

    it("make sure BUY order list is sorted high to low", async () => {
  
        //creating instance of wallet contract
        let dex = await Dex.deployed();
      
        //creating instance of LINK token contract
        let link = await Link.deployed();

        //creating some test buy limit orders
        await truffleAssert.passes(dex.createLimitOrder(Dex.Side.BUY,web3.utils.fromUtf8("LINK"),100,200));
        await truffleAssert.passes(dex.createLimitOrder(Dex.Side.BUY,web3.utils.fromUtf8("LINK"),100,300));
        await truffleAssert.passes(dex.createLimitOrder(Dex.Side.BUY,web3.utils.fromUtf8("LINK"),100,100));
        await truffleAssert.passes(dex.createLimitOrder(Dex.Side.BUY,web3.utils.fromUtf8("LINK"),100,600));

        //obtaining BUY order book for LINK
        let buy_order = await dex.getOrderBook(web3.utils.fromUtf8("LINK"),Dex.Side.BUY);
        assert(buy_order.length > 0);


        //take a look at order book
        //console.log(buy_order);

        //looking through buy_order book to determine if list is sorted from highest to lowest
        for (let i = 1;i < buy_order.length;i++) {
            assert(buy_order[i-1].price >= buy_order[i].price);
        }
    
    })
    it("make sure final price in SELL is lowest value", async () => {
  
        //creating instance of wallet contract
        let dex = await Dex.deployed();
      
        //creating instance of LINK token contract
        let link = await Link.deployed();

        //creating some test limit sell orders
        //creating some test buy limit orders
        await truffleAssert.passes(dex.createLimitOrder(Dex.Side.SELL,web3.utils.fromUtf8("LINK"),100,200));
        await truffleAssert.passes(dex.createLimitOrder(Dex.Side.SELL,web3.utils.fromUtf8("LINK"),100,300));
        await truffleAssert.passes(dex.createLimitOrder(Dex.Side.SELL,web3.utils.fromUtf8("LINK"),100,100));
        await truffleAssert.passes(dex.createLimitOrder(Dex.Side.SELL,web3.utils.fromUtf8("LINK"),200,500));

        //obtaining BUY order book for LINK
        let sell_order = await dex.getOrderBook(web3.utils.fromUtf8("LINK"),Dex.Side.SELL);
        assert(sell_order.length > 0);

        //take a look at order book
        //console.log(sell_order);

        //looking through buy_order book to determine if list is sorted from lowest to highest
        for (let i = 1;i < sell_order.length;i++) {
            assert(sell_order[i-1].price <= sell_order[i].price);
        }
    
    })


})

Test harness for market order tests…

// BUY (ask): user's ETH > buy order value
// SELL (bid): user must have enough tokens such that token amount is greater than sell order amount
//the first order [0] in the BUY book must have highest price
//the last order in SELL book must have lowest price ().length - 1
const Dex = artifacts.require("Dex");
const Link = artifacts.require("Link");
const truffleAssert = require('truffle-assertions');

contract("Dex",accounts => {
    it("should only be possible for owners to add tokens", async () => {
  
        //creating instance of wallet contract
        let dex = await Dex.deployed();
      
        //creating instance of LINK token contract
        let link = await Link.deployed();
        
        //adding token ticker and address to wallet
        //await link.approve(dex.address,2000);
        await truffleAssert.passes(dex.addToken(web3.utils.fromUtf8("LINK"),link.address, {from: accounts[0]}));

        //making sure token cannot be added twice
        await truffleAssert.reverts(dex.addToken(web3.utils.fromUtf8("LINK"),link.address, {from: accounts[0]}));
    })
    it("should handle deposits correctly", async () => {
  
        //creating instance of wallet contract
        let dex = await Dex.deployed();
      
        //creating instance of LINK token contract
        let link = await Link.deployed();

        //depositing 2000 link into dex wallet accounts[0]
        await link.approve(dex.address,2000);
        await dex.deposit(500,web3.utils.fromUtf8("LINK"));
        
        let balance = await dex.balances(accounts[0],web3.utils.fromUtf8("LINK"));

        //checking LINK balance is properly deposited
        assert.equal(balance.toNumber(),500);
        
    })
    it("should be able to submit market order with nothing in order book", async () => {
  
        //creating instance of wallet contract
        let dex = await Dex.deployed();
      
        //creating instance of LINK token contract
        let link = await Link.deployed();

        //submitting market order
        await truffleAssert.passes(dex.createMarketOrder(Dex.Side.BUY,web3.utils.fromUtf8("LINK"),100));
        
    })
    it("market order should fill the correct number of orders in order book", async () => {
  
        //creating instance of wallet contract
        let dex = await Dex.deployed();
      
        //creating instance of LINK token contract
        let link = await Link.deployed();

        //depositing ETH for test
        dex.depositEth({value: 100000});

        //sending link to accounts 1,2,3
        await link.transfer(accounts[1],100);
        await link.transfer(accounts[2],100);
        await link.transfer(accounts[3],100);

        let balance = await link.balanceOf(accounts[1]);
        //console.log(balance.toNumber());

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

        //deposit LINK to dex wallet
        await dex.deposit(100,web3.utils.fromUtf8("LINK"),{from:accounts[1]});
        await dex.deposit(100,web3.utils.fromUtf8("LINK"),{from:accounts[2]});
        await dex.deposit(100,web3.utils.fromUtf8("LINK"),{from:accounts[3]});

        //adding limit orders
        await dex.createLimitOrder(Dex.Side.SELL,web3.utils.fromUtf8("LINK"),20,200,{from:accounts[1]});
        await dex.createLimitOrder(Dex.Side.SELL,web3.utils.fromUtf8("LINK"),20,300,{from:accounts[2]});
        await dex.createLimitOrder(Dex.Side.SELL,web3.utils.fromUtf8("LINK"),20,500,{from:accounts[3]});

        //obtaining SELL order book for LINK
        let _orders = await dex.getOrderBook(web3.utils.fromUtf8("LINK"),Dex.Side.SELL);
        let orderLength = _orders.length;


        //submitting market buy order that clear out order book
        await dex.createMarketOrder(Dex.Side.BUY,web3.utils.fromUtf8("LINK"),40);

        //obtaining NEW SELL order book for LINK
        let new_orders = await dex.getOrderBook(web3.utils.fromUtf8("LINK"),Dex.Side.SELL);
        let new_orderLength = new_orders.length;
        //console.log(new_orders);
        console.log(new_orderLength);

        //1 order should be left
        assert(new_orderLength == 1);
        
    })
    it("big market order should fill all orders and empty out order book", async () => {
  
        //creating instance of wallet contract
        let dex = await Dex.deployed();
      
        //creating instance of LINK token contract
        let link = await Link.deployed();

        //obtaining SELL order book for LINK
        let _orders = await dex.getOrderBook(web3.utils.fromUtf8("LINK"),Dex.Side.SELL);
        let orderLength = _orders.length;
        //console.log(_orders);
        console.log(orderLength);

        //balance before
        let balance = await dex.balances(accounts[0],web3.utils.fromUtf8("LINK"));

        //submitting market buy order larger than sell limit orders left in order book
        await dex.createMarketOrder(Dex.Side.BUY,web3.utils.fromUtf8("LINK"),40);

        //obtaining NEW SELL order book for LINK
        let new_orders = await dex.getOrderBook(web3.utils.fromUtf8("LINK"),Dex.Side.SELL);
        let new_orderLength = new_orders.length;
        //console.log(new_orders);
        console.log(new_orderLength);

        //balance after
        let new_balance = await dex.balances(accounts[0],web3.utils.fromUtf8("LINK"));

        //no order should be left
        assert(new_orderLength == 0);
        //new account[0] should have increased by 20 (since only 1 limit sell order of 20 was left in order book)
        assert.equal(new_balance.toNumber(),balance.toNumber() + 20);
    })
    it("sell market order should have enough tokens for the sale", async () => {
  
        //creating instance of wallet contract
        let dex = await Dex.deployed();
      
        //creating instance of LINK token contract
        let link = await Link.deployed();

        //submitting market sell order that should pass
        await truffleAssert.passes(dex.createMarketOrder(Dex.Side.SELL,web3.utils.fromUtf8("LINK"),30));

        //submitting market sell order that should fail - over 2000 LINK
        await truffleAssert.reverts(dex.createMarketOrder(Dex.Side.SELL,web3.utils.fromUtf8("LINK"),3000));
        
    })
    it("buy market order acount ETH balance drops by the amount of purchase", async () => {
  
        //creating instance of wallet contract
        let dex = await Dex.deployed();
      
        //creating instance of LINK token contract
        let link = await Link.deployed();

        // adding small limit order
        await dex.createLimitOrder(Dex.Side.SELL,web3.utils.fromUtf8("LINK"),1,300,{from:accounts[2]});
         
        //ETH balance before
        let balance = await dex.balances(accounts[0],web3.utils.fromUtf8("ETH"));

        //submitting market buy order that should pass
        await dex.createMarketOrder(Dex.Side.BUY,web3.utils.fromUtf8("LINK"),1);

        //ETH balance after
        let new_balance = await dex.balances(accounts[0],web3.utils.fromUtf8("ETH"));
        
        //check balances
        assert.equal(balance.toNumber()-300,new_balance.toNumber());

    })
    it("limit sell orders LINK should drop by amount of sell", async () => {
  
        //creating instance of wallet contract
        let dex = await Dex.deployed();
      
        //creating instance of LINK token contract
        let link = await Link.deployed();

        //sending link to accounts 1,2,3
        await link.transfer(accounts[1],100);
        await link.transfer(accounts[2],100);

        //approve dex for accounts 1,2
        await link.approve(dex.address,200,{from:accounts[1]});
        await link.approve(dex.address,200,{from:accounts[2]});

        //deposit LINK to dex wallet
        await dex.deposit(100,web3.utils.fromUtf8("LINK"),{from:accounts[1]});
        await dex.deposit(100,web3.utils.fromUtf8("LINK"),{from:accounts[2]});
    
        //adding limit orders
        await dex.createLimitOrder(Dex.Side.SELL,web3.utils.fromUtf8("LINK"),20,200,{from:accounts[1]});
        await dex.createLimitOrder(Dex.Side.SELL,web3.utils.fromUtf8("LINK"),20,300,{from:accounts[2]});
        
        //balance before
        let balance_1 = await dex.balances(accounts[1],web3.utils.fromUtf8("LINK"));
        let balance_2 = await dex.balances(accounts[2],web3.utils.fromUtf8("LINK"));

        //submitting market buy order that should pass
        await dex.createMarketOrder(Dex.Side.BUY,web3.utils.fromUtf8("LINK"),40);

        //balance after
        let new_balance_1 = await dex.balances(accounts[1],web3.utils.fromUtf8("LINK"));
        let new_balance_2 = await dex.balances(accounts[2],web3.utils.fromUtf8("LINK"));
        
        //check balances
        assert.equal(new_balance_1.toNumber(),balance_1.toNumber()-20);
        assert.equal(new_balance_2.toNumber(),balance_2.toNumber()-20);


    })
    it("filled limit orders should be removed from order book", async () => {
  
        //creating instance of wallet contract
        let dex = await Dex.deployed();
      
        //creating instance of LINK token contract
        let link = await Link.deployed();

        //adding limit orders
        await dex.createLimitOrder(Dex.Side.SELL,web3.utils.fromUtf8("LINK"),20,200,{from:accounts[1]});
        await dex.createLimitOrder(Dex.Side.SELL,web3.utils.fromUtf8("LINK"),20,300,{from:accounts[2]});
        await dex.createLimitOrder(Dex.Side.SELL,web3.utils.fromUtf8("LINK"),20,300,{from:accounts[3]});

        //obtaining BUY order book for LINK
        let _orders = await dex.getOrderBook(web3.utils.fromUtf8("LINK"),Dex.Side.SELL);
        let orderLength = _orders.length;
        //console.log(_orders);
        console.log(orderLength);

        //submitting market buy order that will remove 2 limit orders
        await dex.createMarketOrder(Dex.Side.BUY,web3.utils.fromUtf8("LINK"),60);

        //obtaining NEW BUY order book for LINK
        let new_orders = await dex.getOrderBook(web3.utils.fromUtf8("LINK"),Dex.Side.SELL);
        let new_orderLength = new_orders.length;
        //console.log(new_orders);
        console.log(new_orderLength);

        assert.equal(new_orderLength,0);
        
    })

})
1 Like

you should not restrict the limit order function to onlyOwner. noone would be able to use the dex only the one address who deployed it otherwise. Anyone should be able to submit limit and market orders.

1 Like

Hey Everyone,

This is my repo for Dex - DEX
I’ve written the code for createMarketOrder() in a slightly different way.
Hope you find it interesting.

Thanks,
Tanu

2 Likes

Hi guys,

I almost finisht the Dex but I got an error message in my wallet.sol file. It is in the “function withdrawEth” and has to do with the msg.sender.call function. I looked at @thecil his post about the call function but I still dont understand what the point is. Schermafbeelding 2021-07-12 om 19.35.02

Schermafbeelding 2021-07-12 om 19.35.52

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

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

contract Wallet is Ownable{
    
    struct Token{
        bytes32 ticker;
        address tokenAddress;
    }

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

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

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

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

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

    function depositEth() payable external {
        balances[msg.sender][bytes32("ETH")] = balances[msg.sender][bytes32("ETH")] + msg.value;
    }
    
    function withdrawEth(uint _amount) external {
        require(balances[msg.sender][bytes32("ETH")] >= _amount,'withdrawEth: Insuffient balance'); 
        balances[msg.sender][bytes32("ETH")] = balances[msg.sender][bytes32("ETH")]-(_amount);
        msg.sender.call{value:_amount}("");
    }
    

}
1 Like

hey @thomascarl. replace

msg.sender.call{value:_amount}("");

with

msg.sender.transfer(amount)

sjould work now

3 Likes

Yes, here is a nice reply on the differences between them (also its explain why is dangerous to use .call().

Took this resource from: https://ethereum.stackexchange.com/questions/19341/address-send-vs-address-transfer-best-practice-usage.

address.transfer()

  • throws on failure
  • forwards 2,300 gas stipend (not adjustable), safe against reentrancy
  • should be used in most cases as it’s the safest way to send ether

address.send()

  • returns false on failure
  • forwards 2,300 gas stipend (not adjustable), safe against reentrancy
  • should be used in rare cases when you want to handle failure in the contract

address.call.value().gas()()

  • returns false on failure
  • forwards all available gas (adjustable), not safe against reentrancy
  • should be used when you need to control how much gas to forward when sending ether or to call a function of another contract

Detailed version below:

The relative tradeoffs between the use of someAddress.send() , someAddress.transfer() , and someAddress.call.value()() :

  • someAddress.send() and someAddress.transfer() are considered safe against reentrancy. While these methods still trigger code execution, the called contract is only given a stipend of 2,300 gas which is currently only enough to log an event.
  • x.transfer(y) is equivalent to require(x.send(y)) , it will automatically revert if the send fails.
  • someAddress.call.value(y)() will send the provided ether and trigger code execution. The executed code is given all available gas for execution making this type of value transfer unsafe against reentrancy.

Using send() or transfer() will prevent reentrancy but it does so at the cost of being incompatible with any contract whose fallback function requires more than 2,300 gas. It is also possible to use someAddress.call.value(ethAmount).gas(gasAmount)() to forward a custom amount of gas.

One pattern that attempts to balance this trade-off is to implement both a push and pull mechanism, using send() or transfer() for the push component and call.value()() for the pull component.

It is worth pointing out that exclusive use of send() or transfer() for value transfers does not itself make a contract safe against reentrancy but only makes those specific value transfers safe against reentrancy.

More details are here https://consensys.github.io/smart-contract-best-practices/recommendations/#be-aware-of-the-tradeoffs-between-send-transfer-and-callvalue

Reasons for adding transfer() : https://github.com/ethereum/solidity/issues/610

call() can also be used to issue a low-level CALL opcode to make a message call to another contract:

if (!contractAddress.call(bytes4(keccak256("someFunc(bool, uint256)")), true, 3)) {
    revert;
}

The forwarded value and gas can be customized:

contractAddress.call.gas(5000)
    .value(1000)(bytes4(keccak256("someFunc(bool, uint256)")), true, 3);

This is equivalent to using a function call on a contract:

SomeContract(contractAddress).someFunc.gas(5000)
    .value(1000)(true, 3);

Beware of the right padding of the input data in call() https://github.com/ethereum/solidity/issues/2884

transfer() , send() and call() functions are translated by the Solidity compiler into the CALL opcode.

As explained on the Subtleties page in the Ethereum’s wiki:

CALL has a multi-part gas cost:

  • 700 base
  • 9000 additional if the value is nonzero
  • 25000 additional if the destination account does not yet exist (note: there is a difference between zero-balance and nonexistent!)

The child message of a nonzero-value CALL operation (NOT the top-level message arising from a transaction!) gains an additional 2300 gas on top of the gas supplied by the calling account; this stipend can be considered to be paid out of the 9000 mandatory additional fee for nonzero-value calls. This ensures that a call recipient will always have enough gas to log that it received funds.

Carlos Z

4 Likes

This unit test works for me using msg.sender.transfer() instead of the call() function. You can try it also if interested @mcgrane5 @thomascarl :nerd_face:

        it("5. WITHDRAW ETH correctly", async function (){
            await dexInstance.depositEth({from: owner, value:_qtyEth(1)})
            
            await dexInstance.withdrawEth( _qtyEth(1), {from: owner});

            await expectRevert(
                dexInstance.withdrawEth( _qtyEth(1), {from: alfa}),
                "withdrawEth: Insuffient balance"
            );

        });
    let _qtyEth = function(_amount) {
        return (web3.utils.toWei(_amount.toString(), "ether"))
    }

Carlos Z

2 Likes

Thanks @thecil and @mcgrane5. It is more clear to me now.

I do have another question. It is about the testing part. When I run test I got 8 tests passing, one pending and also 8 failing. Maybe I just don’t understand the output but when I compare it to Filip his test results I dont see that many failed test. Down below the screenshot of the failed test, I also pushed my final code to GitHub so maybe someone can check it out.

Schermafbeelding 2021-07-13 om 09.26.36

https://github.com/theavaragejoe/dexfinal.git

1 Like

hey @thomascarl. i will show you a good trick for seeing why your tests fail. I will use the example of this test here

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

Currently your saying this fails. What you should do is try to pinpoint the location or line of code that is causing the test to fail. This may originally seemm hard becayse the truffle-assertion error messages are not that useful. i would imagine its your assert statement. So what i would do is i would comment out everything down to your odernook function call. and console.log. like this

//The BUY order book should be ordered on price from highest to lowest starting at index 0
    it("The BUY order book should be ordered on price from highest to lowest starting at index 0", async () => {
    let dex = await Dex.deployed()
    let link = await Link.deployed()
    await link.approve(dex.address, 500);
    await dex.createLimitOrder(0, web3.utils.fromUtf8("LINK"), 1, 300)
    await dex.createLimitOrder(0, web3.utils.fromUtf8("LINK"), 1, 100)
    await dex.createLimitOrder(0, web3.utils.fromUtf8("LINK"), 1, 200)
    
    let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 0);
    truffleAssert(orderbook.length > 0);
    console.log(orderbook);
  
})

run this i would imagine it works. If it does it will print the orderbook to your terminal screen and that way you will be able to judge if the orderbook is actually sorted by price. However if your test wont even run for this and it still fails. then go back and coment out the limit orders like so

//The BUY order book should be ordered on price from highest to lowest starting at index 0
    it("The BUY order book should be ordered on price from highest to lowest starting at index 0", async () => {
    let dex = await Dex.deployed()
    let link = await Link.deployed()
    await link.approve(dex.address, 500);
   
})

run this and if it runs then you now know the isue lies with your limit order function. its all about finding the line of code that throws the error. Once you know where the error is you will be able to debug more easily. This applies for all your tests so just repeat this process.

EDIT actually i think its fialing because your not depositing any link your only approving. The error is probably thrown when youc create limit orders as u have insuffixent funds

Evan

3 Likes

Hello everyone, finished my DEX project.
GitHub repo: https://github.com/BenasVolkovas/decentralized-exchange

I changed the create market order function part where the program needs to remove filled orders. Now it costs less ether to execute the function. I hope it helps someone.

1 Like

@dan-i, @thecil. I have a question in regards to price feeds. I am looking to include live price charts into my dex frontend. Is there any good APIs for getting data of ethereum and other token prices for example. I know you can use chainlink to get the current price etc in your smart contract. But can you query chainlink to get the entire history of a ethereums price. I was reading into other things like “The graph”. What would yous reccomend. Or perhaps should i should just try to query an API in say like my main.js file.

My plan is to use something like plotly.js to graph the data. If yous know of any better plotting libraries mores suited for the dex application that would be brillaint also . I very adept with matplotlib in python for computational stuff but from what ive been reading online there is no set in stone way to use matplotlib with javascript (except for something called mpld3.js which i may look into but it might be more hassle than its worth)

1 Like

Hey man, for charting you could just use the trading view widgets:
https://www.tradingview.com/widget/

And for easy price tracking you could use https://www.coingecko.com/en/api.

Off course you could also use chainlinks feeds but that will require a complex addition in the contract (you could check the chainlink course to learn how to :nerd_face:)
https://data.chain.link/

Carlos Z

1 Like