Assignment - Limit Order Test

I keep getting errors on the following tests, it does not seem to find the token:


 1) Contract: Dex
       should throw an error if token balance is too low when creating SELL limit order:
     Error: Returned error: VM Exception while processing transaction: revert Token does not exist -- Reason given: Token does not exist.
      at Context.<anonymous> (test\dextest.js:31:19)
      at processTicksAndRejections (internal/process/task_queues.js:93:5)

  2) Contract: Dex
       The SELL order book should be ordered on price from lowest to highest starting at index 0:
     Error: Returned error: VM Exception while processing transaction: revert
      at Context.<anonymous> (test\dextest.js:57:19)
      at runMicrotasks (<anonymous>)
      at processTicksAndRejections (internal/process/task_queues.js:93:5)
1 Like

Line 31 of the dextest.js

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

You need to run npm install truffle-assertions

1 Like

hey @CMar10. Can you share your code preferably a link to your repo or if not just post all of your files here in a post. Also can you try to manually create a contract instance in the truffle develop console and run your functions individually. I would say the prooblem is in your addToken or perhaps an issue in the token file itself?. Share your code anyways an ill gladly take a look I cannot say for sure without seeing your code.

I think it is somewhere in the wallet.sol contract, because it is showing the error for the tokenExist function

pragma solidity >=0.6.0 <0.8.4;

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

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

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

contract Wallet is Ownable {

    using SafeMath for uint256;

    struct Token {

        bytes32 ticker;

        address tokenAddress;

    }

    mapping(bytes32 => Token) public tokenMapping;

    bytes32[] public tokenList;

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

    function addToken(bytes32 ticker, address tokenAddress) onlyOwner external {

        tokenMapping[ticker] = Token(ticker, tokenAddress);

        tokenList.push(ticker);

    }

    function deposit(uint amount, bytes32 ticker) tokenExist(ticker) external {

        

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

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

    }

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

        

        require(balances[msg.sender][ticker] >= amount, "Balance not sufficient");

        

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

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

    }

    function depositEth() payable external {

        balances[msg.sender][bytes32("ETH")] = balances[msg.sender][bytes32("ETH")].add(msg.value);

    }

    

    function withdrawEth(uint amount) external {

        require(balances[msg.sender][bytes32("ETH")] >= amount,'Insuffient balance'); 

        balances[msg.sender][bytes32("ETH")] = balances[msg.sender][bytes32("ETH")].sub(amount);

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

    }

     modifier tokenExist(bytes32 ticker){

        require(tokenMapping[ticker].tokenAddress != address(0), "Token does not exist");

        _;

    }

}

I do get a warning about the Token contract being ā€œabstractā€

pragma solidity >=0.6.0 <0.8.4;

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

contract Link is ERC20 {

    constructor() ERC20("Chainlink", "LINK") public {

        _mint(msg.sender, 1000);

    }

}

I donā€™t know if I over simplified thisā€¦ but, wondering what people think of thisā€¦? Let me know if itā€™s horse shit code lol

contract("DEX", accounts => {
  //user must have ETH deposited and an ETH balance >= buy order values, because we are purchasing tokens WITH ETH
  it("Trader has deposited ETH (in test case, LINK) & has an ETH(LINK) balance >= buy order value", async() => {
    let dex = await DEX.deployed()
    let token = TestToken.deployed()
    await token.approve(dex.address, 500)
    await truffleAssert.reverts(dex.createLimitOrder(web3.utils.fromutf8("LINK"), 0, 750))
    await dex.deposit(500, web.utils.fromUtf8("LINK"))
    await truffleAssert.passes(dex.createLimitOrder(web3.utils.fromutf8("LINK"), 0, 750))

  })

  it("User must have >= amount of tokens in their balance in order to fill their SELL order", async() => {
    let balance = await dex.balances(accounts[0], web3.utils.fromUtf8("LINK"));
    let sellOrder = await dex.createLimitOrder(web3.utils.fromutf8("LINK"), 1, 750);
    await truffleAssert.passes(balance >= sellOrder);

  })

  it("BUY orderbook is in order from Highest to Lowest, starting at index[0]", async() => {
    let buyOrderBook = await dex.orderbook(web3.utils.fromUtf8("LINK"), 0);
    await truffleAssert.passes(buyOrderBook.sort((a, b) => b - a));
  
  })

  }
)

note

In test #1, I used 750 as buy order amount because the migration file mints 500 tokens on deploymentā€¦ So thatā€™s why it passes with the same 750 tokens depositing only 500 (on top of the 500 minted upon deployment)

1 Like

Hey william this seems fine. Howver i have a question did you get rid of the addToken function which adds say the link token the scope of your dex your instance. is this not a requierment in your code?

I like your first test because you test both sidea of the function both before u call it and after. You should try do this everywhere you can. Like a truffle reverts followed by a truffle passes

But your tests look fine you could even add more to testother scenarios but great work. Only other rhi g is in the sorting test you should add way more limit orders and then test if its sorted atm you only have the two limit orders from tthe previous two tests. Good work though

Well, now Iā€™m on to the next lecture and I see Filip has a function in his test.js called dex.depositETHā€¦

Do we have to create our own separate function that deposits ETH?
On top of this, do we have to create a separate mapping that holds these separate ETH balances?

1 Like

For this you can make a deposit eth function and use it to deposit or what you can do is make another token in your token.sol file called eth and add it, approve it and mint it similar to the link token. You token file would look something like this

contract Token is ERC20 {

    //automatically mint eth on contratc creation
    constructor () ERC20("ChainLink", "LINK") {

        _mint(msg.sender, 100000000);
    }

}

contract Eth is ERC20 {

    constructor () ERC20("Ether", "ETH") {

        _mint(msg.sender, 1000000000000);
    }

}

the alternative is to make a deposit eth function i personally did not go this route but the reason filip makes the deposit eth function is because you dont have to approve ether in real applicatios for wallets or other daps like DEXā€™s as you do ERC20 tokens. Your deposit eth func could look something like thus

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

where you make it a payable function and deposit in what ever msg.value you so wish. Doing it this way does not require us to have to add or approve it in the dex instance.

1 Like

Hello all,

Iā€™ve been working more on the functionality of the DEX and experimenting with ways of initilizing the state of the dex before I start adding lots of orders to the book. One thing Iā€™m trying to do that has me stumpped relates to the following:

        let balancesOf = [];
        await console.log(balancesOf);
        var thisBalance;
        for(let counter = 1; counter < accounts.length; counter ++){
            await dex.transferInDex(accounts[counter], "LINK", 100, {from: accounts[0]});
            await dex.transferInDex(accounts[counter], "ETH", 100, {from: accounts[0]});
            thisBalance = await dex.returnBalance(accounts[counter], "LINK");
            let newString = accounts[counter] + "  " + thisBalance;
            balancesOf.push(newString);
        }
        await console.log(balancesOf);

Thatā€™s a snipit of code from my test script, and the test itself passes fine. My question is regarding the access to the dex.returnBalance() function, shown here:

function returnBalance(address accountAddy, string memory ticker) public returns(uint){
        uint thisBalance = balances[accountAddy][ticker];
        return thisBalance;
    }

If it looks like Iā€™ve added all sorts of wacky steps here and there, itā€™s because I likely have. Iā€™m trying to get the readout from the console.log() statement to display ā€œreturnBalanceā€ as an integer, but so far I can only get

[
  '0xd7B9176A6D216C25C8ab4f768687f22dfd9a9FDB  [object Object]',
  '0xea30db418ca0A9ad9fCCA9920dc62AA06ba51cFD  [object Object]',
  '0x3Ea2982b49c7848D06D2DD68e34C2E8947692356  [object Object]',
  '0x09Aa141E276e5DD11C7a9AE9a298B87A633E9E76  [object Object]',
  '0x2D22167F1EfF301B12DAcE16d0D1ac2A7aF10Efb  [object Object]',
  '0x6C6A29FD66E6e7c060F626007cd6d3EB413636a0  [object Object]',
  '0xC2b2607175C454F4E62Ff36171403d5a6857431f  [object Object]',
  '0xd975773b24C0E66178A8484c28Cc4Df5BBFd74EE  [object Object]',
  '0x0ACf01a9797301A832d38e89249d6c436307c0Ed  [object Object]'
]

I did, however, manage to get ā€œ[object Object]ā€ to appear, instead, as NaN as well as ā€˜promise objectā€™, on one occasion.

Iā€™d like to know how to get it to be an integer. Then my kung-fu can be stronger.

I welcome the input and assistance.

Respectfully,
David

1 Like

hey @Melshman if this was me the first thing i would do it to focus on the balance function and see can i get it to display balances so again this would mean going to go to thr truffle develop console and doing the test individually. Can i ask why you are making an array and trying to push all of your accounts to it. I would imagine doing all these extra steps is whats causing that promise to arise.

Try this for me. cgange you balance function to

function returnBalance(address accountAddy, string memory ticker) public view returns (uint) {
        return balances[accountAddy][ticker];
    }

You do not need the uint thisBalance, just return the balance straight up. Anyways then go into the truffle console and do the whole process of adding approving deposting. Then say you deposit 100 eth into account[0], run you above balance function to see if that works before moving onto more complicated things. So after you do that and deposit call the balance function like

 await dex.returnBalance(accounts[0], "ETH")

If this works then your balance function is working. What i would suggest you do is to addopt a new approach. I would just do a lode of deposits into different accounts and then after this assert that the balances are correct. So you could do something like this.

        await dex.deposit(1000, "ETH", {from: accounts[0]})
        await dex.deposit(2000, "ETH", {from: accounts[1]})
    
        account0EthBalance = await dex1.getBalance(accounts[0], "ETH")
        account1EthBalance = await dex1.getBalance(accounts[1], "ETH")
      
        assert(account0EthBalance == 1000)
        assert(account1ETHBalance == 2000)

This approach should work. Later on after your market order when yu get to that you could test both sides of this function by asserting bal = 1000 then creating a limit and market order, waiting for the orders to settle and get trasnsfered to the owneers and the asserting that the balances have updated correctly again after the operation.

In you code. I would say that there probably is some way to ectract the data from your promise but i am not sure how to do that. But i dthink the approach i ourlined is a little simpler. However if you want to continue using your approach it would be great practice if you could get it working. Perhaps @dan-i would be able to help you more with the promise aspect

2 Likes

Hey @mcgrane5,

Good call on being able to just use asserts to check that the state-changes Iā€™m trying to make with my test script. However, now that Iā€™m down this rabbit-hope, I canā€™t shake it. Plus it obviously exposes a pretty large hole in my JS knowledge. The ONLY reason Iā€™m doing things this way is because I had a notion about how I could set up testing, and Iā€™m still running with it.

I had already tried returning just

balances[accountAddy][ticker]

without first assigning it to a uint, but it still produces the exact same results. Iā€™ve been flinging ā€œawaitā€ everywhere to see what sticks :joy:

Iā€™ve even managed to get this readout recently, after changing the structure of the JS loop:
I put the ā€œthisBalanceā€ variable into a ā€œthisBalFuncā€ value like so:

let thisBalFunc = async () => {
                let thisBalance = await dex.returnBalance(accounts[counter], "LINK");
            }

And when I run the test afterward, the balanceOf array looks like this:

[
  '0xd7B9176A6D216C25C8ab4f768687f22dfd9a9FDB  async () => {\r\n' +
    '                let thisBalance = await dex.returnDexBalances(accounts[counter], "LINK");\r\n' +
    '            }',
  '0xea30db418ca0A9ad9fCCA9920dc62AA06ba51cFD  async () => {\r\n' +
    '                let thisBalance = await dex.returnDexBalances(accounts[counter], "LINK");\r\n' +
    '            }',
  '0x3Ea2982b49c7848D06D2DD68e34C2E8947692356  async () => {\r\n' +
    '                let thisBalance = await dex.returnDexBalances(accounts[counter], "LINK");\r\n' +
    '            }',
  '0x09Aa141E276e5DD11C7a9AE9a298B87A633E9E76  async () => {\r\n' +
    '                let thisBalance = await dex.returnDexBalances(accounts[counter], "LINK");\r\n' +
    '            }',
  '0x2D22167F1EfF301B12DAcE16d0D1ac2A7aF10Efb  async () => {\r\n' +
    '                let thisBalance = await dex.returnDexBalances(accounts[counter], "LINK");\r\n' +
    '            }',
  '0x6C6A29FD66E6e7c060F626007cd6d3EB413636a0  async () => {\r\n' +
    '                let thisBalance = await dex.returnDexBalances(accounts[counter], "LINK");\r\n' +
    '            }',
  '0xC2b2607175C454F4E62Ff36171403d5a6857431f  async () => {\r\n' +
    '                let thisBalance = await dex.returnDexBalances(accounts[counter], "LINK");\r\n' +
    '            }',
  '0xd975773b24C0E66178A8484c28Cc4Df5BBFd74EE  async () => {\r\n' +
    '                let thisBalance = await dex.returnDexBalances(accounts[counter], "LINK");\r\n' +
    '            }',
  '0x0ACf01a9797301A832d38e89249d6c436307c0Ed  async () => {\r\n' +
    '                let thisBalance = await dex.returnDexBalances(accounts[counter], "LINK");\r\n' +
    '            }'

It doesnā€™t seem very useful, but it gives me a little insight on what may be happening (aside from that big hole in JS knowledge Iā€™m steadily filling in).

The newest, and hopefully easiest to resolve, is this error I get when typing commands into the develop console:

truffle(develop)> await dex.returnBalance(accounts[0], "ETH")
Uncaught TypeError: dex.returnBalance is not a function

But I assure you, Iā€™ve checked my spelling about a zillion times now, and Iā€™ve definitely included ā€œreturnBalance()ā€ as a function in my Dex contract:

function returnBalance(address accountAddy, string memory ticker) public view returns(uint){
        return balances[accountAddy][ticker];
    }

I wonder if it has something to do with not being able to resolve the promise (not 100% what Iā€™m saying right now is the proper way to put it) because of how my returnBalance() function is receiving its inputs from the truffle environment.

My general feeling is that there are two relatively smiple issues acting in tandem, but itā€™s currently difficult to sort out. Iā€™m looking VERY forward to whatever epiphany can be provided :smile:

(getting so close! Then project timeā€¦!)

2 Likes
Add token test
it("should only be possible for the owner to add tokens", async () => {
       truffleAssert.passes(dex.addToken(web3.utils.fromUtf8("LINK"), link.address, {from: accounts[0]}));
    });
Add buy order test
it("should only be possible to add orders when the users deposits are larger or equal to the order cost", async () => {
        truffleAssert.reverts(dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 1, 50, 0, {from: accounts[0]}));
});
Add sell order test

This text will be hidden

Buy order book sorting test
it("The buy order book should be ordered by price from highest to lowest", async () => {
        
      dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 4, 1, 0);
      dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 12, 1, 0);
      dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 3, 1, 0);
      dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 4, 3, 0);

      let buyOrderBookIsSorted;
      await isOrderBookSorted(0).then(isSorted => {buyOrderBookIsSorted = isSorted});

      assert.equal(buyOrderBookIsSorted, true , "Order book is unordered"); 
});
Sell order book sorting test
    it("The sell order book should be ordered by price from highest to lowest", async () => {
        
        dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 4, 1, 1);
        dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 12, 1, 1);
        dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 3, 1, 1);
        dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 4, 3, 1);

        let sellOrderBookIsSorted;
        await isOrderBookSorted(1).then(isSorted => {sellOrderBookIsSorted = isSorted});

        
        assert.equal(sellOrderBookIsSorted, true, "Order book is unordered");
    });
isOrderBookSorted
async function isOrderBookSorted(side){
        let isSorted = true;

        await dex.getOrderBook(web3.utils.fromUtf8("LINK"), side).then(orderBook => {
            let previousOrder = orderBook[0];

            orderBook.map(order => {
                isSorted = (order.price > previousOrder.price) && (isSorted);
                
                previousOrder = order; 
            });
        });

        return isSorted;
}
1 Like

hey @Melshman i will take another look into this and try to get your way of doing things working. However i will not have the time tonight i will get to it sometime over the weekend if thats ok. I think you linked your repo somewhere awhile back so ill just clone it and adjust your tests accodingly as per your recent posts. I will get back to you soon before the end the wekend hopefully.

Evan

I cannot figure out what in the world is going on. I get 2 tests failing:

  1. Contract: DEX
    BUY orderbook is in order from Highest to Lowest, starting at index[0]:
    AssertionError: buy-order book is out of order.
    at Context. (test/DexTest.js:81:7)
    at processTicksAndRejections (internal/process/task_queues.js:95:5)

  2. Contract: DEX
    SELL orderbook is in order from Lowest to Highest, starting at index[0]:
    Error: Returned error: VM Exception while processing transaction: revert
    at Context. (test/DexTest.js:92:15)
    at processTicksAndRejections (internal/process/task_queues.js:95:5)

Here is the create Limit order test:

function createLimitOrder(bytes32 ticker, SIDE _side, uint amount, uint price) public tokenExists(ticker) {

    if(_side == SIDE.BUY){
      require(EthBalance[msg.sender] >= amount.mul(price));

    }
    else if(_side == SIDE.SELL){
      require(balances[msg.sender][ticker] >= amount);

    }

    TransactionOrder[] storage transactionOrders = orderbook[ticker][uint(side)];
    //This TxOrder (below) will be pushed into the TxOrder[] above^, we've already extracted if it's Buy/Sell
    transactionOrders.push(TransactionOrder(nextOrderId, msg.sender, _side, ticker, amount, price));

    //bubble sort
    //[6, 5, 4, 3]
    //setting the last element in the array to i using an if statement as a variable
    uint i = transactionOrders.length > 0 ? transactionOrders.length - 1 : 0;
    if (_side == SIDE.BUY){
      while(i > 0){
        //if the last element.price minus 1 > than the last element (i) in the array, then...
        //array is sorted properly
        if(transactionOrders[i - 1].price > transactionOrders[i].price){
          break;
        }
        TransactionOrder memory orderToMove = transactionOrders[i - 1];
        transactionOrders[i - 1] = transactionOrders[i];
        transactionOrders[i] = orderToMove;
          //decrease value of i and do it again...
        i--;
      }
    }
    //[2, 3, 3]
    //sellOrderToMove = 5
    if (_side == SIDE.SELL){
      while(i > 0){
        if(transactionOrders[i - 1].price < transactionOrders[i].price){
          break;
        }
        TransactionOrder memory sellOrderToMove = transactionOrders[i - 1];
        transactionOrders[i - 1] = transactionOrders[i];
        transactionOrders[i] = sellOrderToMove;
        i--;
      }
    }
  }

and here is my test for Buy order in order and sell orders in order

it("BUY orderbook is in order from Highest to Lowest, starting at index[0]", async() => {
    let dex = await DEX.deployed()
    let token = await TestToken.deployed()
    await token.approve(dex.address, 600);
    await dex.depositETH({value: 5});
    await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 0, 100, 2)
    await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 0, 250, 1)
    await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 0, 200, 4)
    let buyOrderBook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 0);
    assert(buyOrderBook.length > 0);
    console.log(buyOrderBook);
    for (let i = 0; i < buyOrderBook.length - 1; i++){
      assert(buyOrderBook[i].price >= buyOrderBook[i + 1].price, "buy-order book is out of order.")
    }

it("SELL orderbook is in order from Lowest to Highest, starting at index[0]", async() => {
    let dex = await DEX.deployed()
    let token = await TestToken.deployed()
    await token.approve(dex.address, 600);
    await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 1, 100, 1)
    await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 1, 250, 1)
    await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 1, 200, 1)
    let sellOrderBook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1);
    assert(sellOrderBook.length > 0);
    for (let i = 0; i < sellOrderBook.length - 1; i++){
      assert(sellOrderBook[i].price <= sellOrderBook[i + 1].price, "sell-order book is out of order.")
    }
  })

I console.log() the array for buy orders and the prices all come back in orderā€¦

and the SELL orderbook doesnā€™t show any orders are loggedā€¦ when I console.log() the sell order book, it returns an empty array --> []

1 Like

Hey @William will take a look into this when i get home

also, when I run the code in truffle just testing things outā€¦ theres no limit to how many orders I can place if I am SELLING using createLimitOrder()ā€¦

on the flip side, createLimitOrder() for BUYING doesnā€™t work at all, just throws a nasty error.

Yet, when I call getOrderbook()ā€¦ the Selling ordering book shows empty, yet the buy order book is filled with the infinite amount of createLimitOrders I triedā€¦

what the hellā€¦?

heres my entire DEX contract:

pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;

import "./Wallet.sol";

contract DEX is Wallet {

  using SafeMath for uint256;

  enum SIDE {BUY, SELL}

  SIDE side;

  //Building the transaction information
  struct TransactionOrder {
    uint id;
    address trader;
    SIDE side;
    bytes32 ticker;
    uint amount;
    uint price;
  }

  uint public nextOrderId = 0;

  //need an ORDERBOOK split into two parts -->
  //bids & asks
  //AND an orderbook for each asset

  mapping (address => uint256) public EthBalance;

  //order points to a ticker which points to a Enum option (uint 0, 1, etc)
  mapping (bytes32 => mapping (uint => TransactionOrder[])) public orderbook;

  //depositing ETH requires the dex.depositETH({value: X}); command
  function depositETH() public payable returns(bool) {
    require(msg.value > 0, "Insufficient value.");
    EthBalance[msg.sender]= EthBalance[msg.sender].add(msg.value);
    return true;

  }

  //get the orderbook --> need the bytes32 ticker and the SIDE (BUY OR SELL)
  //view because it just returns something
  //and to input this function --> an example = getOrderBook(bytes32("LINK"), SIDE.BUY);
    //Solidity automatically converts SIDE.BUY into an integer that reads "0" or "1"
      //with order they are presented in enum
  function getOrderBook (bytes32 ticker, SIDE _side) view public returns(TransactionOrder[] memory) {
    return orderbook[ticker][uint(_side)];
  }

  //Complex function to create... why?
  //as soon as you add an order into the order book, you need to sort it --> needs to be in proper position
    //best Buy price is at HIGHEST side of BIDS orderbook
    //best Sell price is at LOWEST side of ASKS orderbook
  //loops are needed for this

  //args (ticker, uint 0/1 = buy/sell, uint how many tokens do you want to buy/sell, uint price of token)
  function createLimitOrder(bytes32 ticker, SIDE _side, uint amount, uint price) public tokenExists(ticker) {

    if(_side == SIDE.BUY){
      require(EthBalance[msg.sender] >= amount.mul(price));

    }
    else if(_side == SIDE.SELL){
      require(balances[msg.sender][ticker] >= amount);

    }

    TransactionOrder[] storage transactionOrders = orderbook[ticker][uint(side)];
    //This TxOrder (below) will be pushed into the TxOrder[] above^, we've already extracted if it's Buy/Sell
    transactionOrders.push(TransactionOrder(nextOrderId, msg.sender, _side, ticker, amount, price));

    //bubble sort
    //[6, 5, 4, 3]
    //setting the last element in the array to i using an if statement as a variable
    uint i = transactionOrders.length > 0 ? transactionOrders.length - 1 : 0;
    if (_side == SIDE.BUY){
      while(i > 0){
        //if the last element.price minus 1 > than the last element (i) in the array, then...
        //array is sorted properly
        if(transactionOrders[i - 1].price > transactionOrders[i].price){
          break;
        }
        TransactionOrder memory orderToMove = transactionOrders[i - 1];
        transactionOrders[i - 1] = transactionOrders[i];
        transactionOrders[i] = orderToMove;
          //decrease value of i and do it again...
        i--;
      }
    }
    //[2, 3, 3]
    //sellOrderToMove = 5
    if (_side == SIDE.SELL){
      while(i > 0){
        if(transactionOrders[i - 1].price < transactionOrders[i].price){
          break;
        }
        TransactionOrder memory sellOrderToMove = transactionOrders[i - 1];
        transactionOrders[i - 1] = transactionOrders[i];
        transactionOrders[i] = sellOrderToMove;
        i--;
      }
    }
  }


}

and heres the token contract:

pragma solidity ^0.8.0;

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

contract TestToken is ERC20 {

  constructor() ERC20("Chainlink", "LINK") {
      _mint(msg.sender, 1000);
  }

}

and finally the wallet contract DEX inherits:

pragma solidity ^0.8.0;


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


//This should be Ownable
contract Wallet is Ownable {

  using SafeMath for uint256;

  event Deposit(address sender, uint amount, uint balance);

  //Since we are interacting with other ERC20 token contracts,
  //we need to have a way to store information about these different tokens

  //WHY "tokenAddress?" --> whenever you create something
  //you need to be able to do transfer calls WITH that created ERC20 contract

  struct Token {
    bytes32 ticker;
    address tokenAddress;
  }

  //In order for the DEX to be able to trade the token later on,
  //it needs support for that token (needs the tokenAddress saved somewhere) --
  //SAVE this address in a combined structure between an array and a mapping

  //can get the tokens and update them quickly here
  mapping (bytes32 => Token) public tokenMapping;

  //Saves all of the tickers (unique)
  //can loop through all of the tokens, just can't delete
  bytes32[] public tokenList;


  //create a double mapping for the balances (every user/trader will have a balance of different tokens)
  //will be able to deposit both ETH and ERC20 tokens (can have ETH, LINK, AAVE, etc...)
  //need a mapping that supports multiple balances
  //mapping is an address that points to another mapping that holds the tokenID (expressed with bytes32) and amount
  //Why bytes32? B/C in Solidity, you can't compare strings (can't string = string)
  //Instead, you can convert the "token symbol" into bytes32
  mapping (address => mapping(bytes32 => uint256)) public balances;

  //instead of adding a bunch of require() statements that chekc for the same thing
  //we will create a modifier and just add it to the function header, remove the require code from body
  modifier tokenExists(bytes32 ticker) {
    require(tokenMapping[ticker].tokenAddress != address(0));
    _;
  }

  //Create an ADD token FUNCTION so we can add to our DEX
  //bytes32 ticker --> give it it's ticker symbol and the bytes32 makes it comparable (can't do string = string)
  //address tokenAddress --> to access this token's "home" contract to interact with it
  //why external? Won't need to execute this from inside this contract
  function addToken(bytes32 ticker, address tokenAddress) onlyOwner external {
    tokenMapping[ticker] = Token(ticker, tokenAddress);
    tokenList.push(ticker);
  }

  //Pull cryptoassets in FROM another contract address
  //increase depositer balances[address][ticker] =  balances[address][ticker].add(amount)
  function deposit(uint amount, bytes32 ticker) tokenExists(ticker) external {
    IERC20(tokenMapping[ticker].tokenAddress).transferFrom(msg.sender, address(this), amount);
    balances[msg.sender][ticker] = balances[msg.sender][ticker].add(amount);
    //emit Deposit(msg.sender, msg.value, address(this).balance);
  }

  //Why do you check if the tokenAddress is not the 0 address?
  //If this mapping of the specific ticker points to an unitialized struct, all of the data will be 0

  function withdraw(uint amount, bytes32 ticker) tokenExists(ticker) external {
    require(balances[msg.sender][ticker] >= amount, "Balance not sufficient.");
    balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(amount);
    //IERC20(address of the token in THIS contract).transfer(recipient address of where it's going, amount)
    IERC20(tokenMapping[ticker].tokenAddress).transfer(msg.sender, amount);

  }

}

1 Like

hey @William. Sorry for getting back so late but I had a look at your code there and there was a few things. So firstly the main reason you were getting bazzar outputs akin to what you described above was because of a small typo that was messing up the sell side of your order book (which actuallty took me awhile to find). The bug or rather typo, lied withtin the createLimitOrder( ) function. The problem is that when you created an instance of the orderbook i.e assinging it to some var you made a mistake when your inputted your orderbook ā€œsideā€. You originally had written

 TransactionOrder[] storage transactionOrders = orderbook[ticker][uint(side)];

should have been

 TransactionOrder[] storage transactionOrders = orderbook[ticker][uint(_side)];

So all you needed was to remain consistent with your var names. This is the reason for all of your unexpected order book errors. Previously the way you had it the sell orders never got appended and this is what caused your tests to fail. This little bug is the reason why the orderboook on the sell side would not update. Instead what was happening was all of the orders were going into the same orderbook well thats what it looked like to me from terminal output. Regardless that is the fix

Now onto the tests. Tour tests needed some minor adjustments too. Firstly you were approving the ā€œlinkā€ token for use, but you never added it. You need to add the token before you can approve it. Also when i used your original limit orders, they failed because the summation of the price multiplied by th eorginal amounts you had used, went above the amount of tokens that you had approved for the link token. So i just changed you amounts and prices to be below the aprrove allowance. I also just deposited link in the sell order test. I know you mint 1000 on the contract creation but i just deposited link tokens in this test anyways. Other than that there was very minimal changes i made to your code but the files are below, the main thing was just the typo in your smart contract,

TransactionOrder[] storage transactionOrders = orderbook[ticker][uint(_side)];

TEST FILE

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

contract("Dex", accounts => {
    
    it("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 Token.deployed()

        //add token to the exchane first
        dex.addToken( web3.utils.fromUtf8("LINK"), link.address);
        await link.approve(dex.address, 500);
        await dex.deposit(200,  web3.utils.fromUtf8("LINK"))
        await dex.depositETH({value: 3000});
        await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 1, 1, 300)
        await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 1, 1, 100)
        await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 1, 1, 200)

        let buyOrderBook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1);
        assert(buyOrderBook.length > 0);
        // console.log(buyOrderBook);
        for (let i = 0; i < buyOrderBook.length - 1; i++) {
            assert(buyOrderBook[i].price <= buyOrderBook[i+1].price)
        }
    })

    it("SELL OrderBook is in order from Lowest to Highest, starting at index[0]", async() => {
        let dex = await Dex.deployed()
        let link = await Token.deployed()
        await dex.addToken(web3.utils.fromUtf8("LINK"), link.address)
        await link.approve(dex.address, 1000);
        await dex.depositETH({from: accounts[0], value: 1000});
        await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 0, 1, 200)
        await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 0, 1, 100)
        await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 0, 2, 400)
        let sellOrderBook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 0);
        // assert(buyOrderBook.length > 0);
        console.log(sellOrderBook);
        for (let i = 0; i < sellOrderBook.length - 1; i++){
            // console.log(sellOrderBook[i]);
          assert(sellOrderBook[i].price >= sellOrderBook[i + 1].price, "buy-order book is out of order.")
        }
        // console.log(sellOrderBook.length);
    })
    
    

})

DEX

pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;

import "./Wallet.sol";

contract Dex is Wallet {

  using SafeMath for uint256;

  enum SIDE {BUY, SELL}

  SIDE side;

  //Building the transaction information
  struct TransactionOrder {
    uint id;
    address trader;
    SIDE side;
    bytes32 ticker;
    uint amount;
    uint price;
  }

  uint public nextOrderId = 0;

  //need an ORDERBOOK split into two parts -->
  //bids & asks
  //AND an orderbook for each asset

  mapping (address => uint256) public EthBalance;

  //order points to a ticker which points to a Enum option (uint 0, 1, etc)
  mapping (bytes32 => mapping (uint => TransactionOrder[])) public orderbook;

  //depositing ETH requires the dex.depositETH({value: X}); command
  function depositETH() public payable returns(bool) {
    require(msg.value > 0, "Insufficient value.");
    EthBalance[msg.sender]= EthBalance[msg.sender].add(msg.value);
    return true;

  }

  //get the orderbook --> need the bytes32 ticker and the SIDE (BUY OR SELL)
  //view because it just returns something
  //and to input this function --> an example = getOrderBook(bytes32("LINK"), SIDE.BUY);
  //Solidity automatically converts SIDE.BUY into an integer that reads "0" or "1"
   //with order they are presented in enum
  function getOrderBook (bytes32 ticker, SIDE _side) view public returns(TransactionOrder[] memory) {
    return orderbook[ticker][uint(_side)];
  }

  //Complex function to create... why?
  //as soon as you add an order into the order book, you need to sort it --> needs to be in proper position
    //best Buy price is at HIGHEST side of BIDS orderbook
    //best Sell price is at LOWEST side of ASKS orderbook
  //loops are needed for this

  //args (ticker, uint 0/1 = buy/sell, uint how many tokens do you want to buy/sell, uint price of token)
  function createLimitOrder(bytes32 ticker, SIDE _side, uint amount, uint price) public tokenExists(ticker) {

    if(_side == SIDE.BUY){
      require(EthBalance[msg.sender] >= amount.mul(price));

    }
    else if(_side == SIDE.SELL){
      require(balances[msg.sender][ticker] >= amount);

    }

    TransactionOrder[] storage transactionOrders = orderbook[ticker][uint(_side)];
    //This TxOrder (below) will be pushed into the TxOrder[] above^, we've already extracted if it's Buy/Sell
    transactionOrders.push(TransactionOrder(nextOrderId, msg.sender, _side, ticker, amount, price));

    //bubble sort
    //[6, 5, 4, 3]
    //setting the last element in the array to i using an if statement as a variable
    uint i = transactionOrders.length > 0 ? transactionOrders.length - 1 : 0;
    if (_side == SIDE.BUY){
      while(i > 0){
        //if the last element.price minus 1 > than the last element (i) in the array, then...
        //array is sorted properly
        if(transactionOrders[i - 1].price > transactionOrders[i].price){
          break;
        }
        TransactionOrder memory orderToMove = transactionOrders[i - 1];
        transactionOrders[i - 1] = transactionOrders[i];
        transactionOrders[i] = orderToMove;
          //decrease value of i and do it again...
        i--;
      }
    }
    //[2, 3, 3]
    //sellOrderToMove = 5
    if (_side == SIDE.SELL){
      while(i > 0){
        if(transactionOrders[i - 1].price < transactionOrders[i].price){
          break;
        }
        TransactionOrder memory sellOrderToMove = transactionOrders[i - 1];
        transactionOrders[i - 1] = transactionOrders[i];
        transactionOrders[i] = sellOrderToMove;
        i--;
      }
    }
  }


}

TOKEN

pragma solidity ^0.8.0;

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

contract Token is ERC20 {

  constructor() ERC20("Chainlink", "LINK") {
      _mint(msg.sender, 1000);
  }

}

Wallet

pragma solidity ^0.8.0;


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


//This should be Ownable
contract Wallet is Ownable {

  using SafeMath for uint256;

  event Deposit(address sender, uint amount, uint balance);

  //Since we are interacting with other ERC20 token contracts,
  //we need to have a way to store information about these different tokens

  //WHY "tokenAddress?" --> whenever you create something
  //you need to be able to do transfer calls WITH that created ERC20 contract

  struct Token {
    bytes32 ticker;
    address tokenAddress;
  }

  //In order for the DEX to be able to trade the token later on,
  //it needs support for that token (needs the tokenAddress saved somewhere) --
  //SAVE this address in a combined structure between an array and a mapping

  //can get the tokens and update them quickly here
  mapping (bytes32 => Token) public tokenMapping;

  //Saves all of the tickers (unique)
  //can loop through all of the tokens, just can't delete
  bytes32[] public tokenList;


  //create a double mapping for the balances (every user/trader will have a balance of different tokens)
  //will be able to deposit both ETH and ERC20 tokens (can have ETH, LINK, AAVE, etc...)
  //need a mapping that supports multiple balances
  //mapping is an address that points to another mapping that holds the tokenID (expressed with bytes32) and amount
  //Why bytes32? B/C in Solidity, you can't compare strings (can't string = string)
  //Instead, you can convert the "token symbol" into bytes32
  mapping (address => mapping(bytes32 => uint256)) public balances;

  //instead of adding a bunch of require() statements that chekc for the same thing
  //we will create a modifier and just add it to the function header, remove the require code from body
  modifier tokenExists(bytes32 ticker) {
    require(tokenMapping[ticker].tokenAddress != address(0));
    _;
  }

  //Create an ADD token FUNCTION so we can add to our DEX
  //bytes32 ticker --> give it it's ticker symbol and the bytes32 makes it comparable (can't do string = string)
  //address tokenAddress --> to access this token's "home" contract to interact with it
  //why external? Won't need to execute this from inside this contract
  function addToken(bytes32 ticker, address tokenAddress) onlyOwner external {
    tokenMapping[ticker] = Token(ticker, tokenAddress);
    tokenList.push(ticker);
  }

  //Pull cryptoassets in FROM another contract address
  //increase depositer balances[address][ticker] =  balances[address][ticker].add(amount)
  function deposit(uint amount, bytes32 ticker) tokenExists(ticker) external {
    IERC20(tokenMapping[ticker].tokenAddress).transferFrom(msg.sender, address(this), amount);
    balances[msg.sender][ticker] = balances[msg.sender][ticker].add(amount);
    //emit Deposit(msg.sender, msg.value, address(this).balance);
  }

  //Why do you check if the tokenAddress is not the 0 address?
  //If this mapping of the specific ticker points to an unitialized struct, all of the data will be 0

  function withdraw(uint amount, bytes32 ticker) tokenExists(ticker) external {
    require(balances[msg.sender][ticker] >= amount, "Balance not sufficient.");
    balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(amount);
    //IERC20(address of the token in THIS contract).transfer(recipient address of where it's going, amount)
    IERC20(tokenMapping[ticker].tokenAddress).transfer(msg.sender, amount);

  }

}

**EDIT i actually sorry i chaged a few of your dex and token contract var names only just because the project folder that i use to debug other peoples code sort of has a format and i just changed your contract var names. Just mentioning thus incase you were wondering why.

xxx

2 Likes

Hi Evan, Thank you so much for going through this for me. I felt i hit a wall, but this defnitiely pushed me through and I feel a sense of liberation in being able to continue!! Thank youā€¦

A quick question, in you tests, your createLimitOrder inputs for enum Buy is a uint1, and for sell you use uint 0ā€¦

In my code, I have enum SIDE {BUY, SELL}

ā€¦so, wouldnā€™t this make the BUY represented by a uint 0? and the Sell and uint 1?

thanks again manā€¦

1 Like