Project - Building a DEX

ohhh my bad i actually cant remember the video yeah he may have just mis said it or something

1 Like

Here’s my solution for the market order tests:

   describe("Market order tests", function(){
        it("Should throw an error when submitting a SELL order with insufficient token balances.", async () => {
            let balance = await dex.balances(accounts[0], LINKB32);
            assert.equal(parseInt(balance), 0, "Initial LINK balance not 0");

            await truffleAssert.reverts(
                dex.createMarketOrder(Side.SELL, LINKB32, 10)
            );
        });

        it("Should throw an error when submitting a BUY order with insufficient ETH balance", async () => {
            let balance = await dex.balances(accounts[0], ETHB32);
            assert.equal(parseInt(balance), 0, "Initial ETH balance not 0");

            await truffleAssert.reverts(
                dex.createMarketOrder(Side.BUY, LINKB32, 10)
            );

        });

        it("Should be possible to submit market orders even if the order books are empty", async () => {
            dex.addFunds({from: accounts[0], value: ETH10});

            let orderBook = dex.getOrderBook(LINKB32, Side.BUY);
            assert(orderBook.length == 0, "Buy side order book not empty!");

            await truffleAssert.passes(
                dex.createMarketOrder(Side.BUY, LINKB32, 10)
            );
        });

        it("A market order should be filled until complete, or the order book is empty", async () => {
            let orderBook = dex.getOrderBook(LINKB32, Side.BUY);
            assert(orderBook.length == 0, "Buy side order book not empty!");

            // We need a couple of SELL orders to be able to fill the buy order.
            // We'll do this from 3 different accounts, with 3 different prices.

            await dex.addToken(LINKB32, link.address);

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

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

            await dex.deposit(10, LINKB32, {from:accounts[1]});
            await dex.deposit(10, LINKB32, {from:accounts[2]});
            await dex.deposit(10, LINKB32, {from:accounts[3]});

            await dex.createLimitOrder(LINKB32, Side.SELL, 10, 100, {from: accounts[1]});
            await dex.createLimitOrder(LINKB32, Side.SELL, 10, 200, {from: accounts[2]});
            await dex.createLimitOrder(LINKB32, Side.SELL, 10, 300, {from: accounts[3]});

            await dex.createMarketOrder(Side.BUY, LINKB32, 20);
            
            // This should be filled with the first two sell orders, leaving one with amount 10.
            orderBook = dex.getOrderBook(LINKB32, Side.BUY);
            assert(orderBook.length == 0, "Buy market order was not filled");

            orderBook = dex.getOrderBook(LINKB32, Side.SELL);
            assert(orderBook.length == 1 && orderBook[0].amount == 10, "Sell side order[0] has unexpected amount. Should be 10 is :" + orderBook[0].amount);

            // And we'll check if the order is filled until the order book is empty.
            await dex.createMarketOrder(Side.BUY, LINKB32, 15);

            orderBook = dex.getOrderBook(LINKB32, Side.SELL);
            assert(orderBook.length == 0, "Sell side order book should be empty");

            orderBook = dex.getOrderBook(LINKB32, Side.BUY);
            assert(orderBook.length == 1 && orderBook[0].amount == 5, "Buy market order[0] has unexpected amount. Should be 5 is :" + orderBook[0].amount);            
        });    

        it("The filled orders should be removed from the order book", async () =>{
            let orderBook = dex.getOrderBook(LINKB32, Side.BUY);
            assert(orderBook.length == 0, "Buy side order book not empty!");

            // We need a couple of SELL orders to be able to fill the buy order.
            // We'll do this from 3 different accounts, with 3 different prices.

            await dex.addToken(LINKB32, link.address);

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

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

            await dex.deposit(10, LINKB32, {from:accounts[1]});
            await dex.deposit(10, LINKB32, {from:accounts[2]});
            await dex.deposit(10, LINKB32, {from:accounts[3]});

            await dex.createLimitOrder(LINKB32, Side.SELL, 10, 100, {from: accounts[1]});
            await dex.createLimitOrder(LINKB32, Side.SELL, 10, 200, {from: accounts[2]});
            await dex.createLimitOrder(LINKB32, Side.SELL, 10, 300, {from: accounts[3]});

            await dex.createMarketOrder(Side.BUY, LINKB32, 20);
            
            // This should be filled with the first two sell orders, leaving one with amount 10.
            orderBook = dex.getOrderBook(LINKB32, Side.SELL);
            assert(orderBook.length == 1, "Two filled sell order should have been removed. Order book length should be 1, is :" + orderBook.length);    
        });

        it("The balances should adjust as the orders get filled", async () =>{
            // We need a couple of SELL orders and a BUY oder, to compare balances.
            // We'll do this from 3 different accounts, with 3 different prices.

            await dex.addToken(LINKB32, link.address);

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

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

            await dex.deposit(10, LINKB32, {from:accounts[1]});
            await dex.deposit(10, LINKB32, {from:accounts[2]});
            await dex.deposit(10, LINKB32, {from:accounts[3]});

            // Save the balances of account 1 and 2 for comparison after we submitted the market BUY order

            let balanceOne = parseInt(await.dex.balances(accounts[1], LINKB32));
            let balanceTwo = parseInt(await.dex.balances(accounts[2], LINKB32));

            await dex.createLimitOrder(LINKB32, Side.SELL, 10, 100, {from: accounts[1]});
            await dex.createLimitOrder(LINKB32, Side.SELL, 10, 200, {from: accounts[2]});
            await dex.createLimitOrder(LINKB32, Side.SELL, 10, 300, {from: accounts[3]});

            await dex.createMarketOrder(Side.BUY, LINKB32, 15);

            let balanceOnePost = parseInt(await.dex.balances(accounts[1], LINKB32));
            let balanceTwoPost = parseInt(await.dex.balances(accounts[2], LINKB32));
            assert(balanceOne - 10 == balanceOnePost && balanceTwo - 5 == balanceTwoPost, "Balances not adjusted correctly");
        });
    });
} );
1 Like

Hi @mcgrane5,

Hope you’re doing great.
Can you please provide your suggestions for the following codes? I haven’t watched the solution video yet. :innocent:

Thanks,
Tanu

dextest.js for Market orders

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

//function createMarketOrder(Side side, bytes32 ticker, uint amount)
contract("Dex - Market Orders",async accounts => {
it("seller needs to have enough tokens for the trade", async () => {
   let dex = await Dex.deployed();
   let link = await Link.deployed();

   await truffleAssert.reverts(dex.createMarketOrder(1,web3.utils.fromUtf8("LINK"),35)); //sell market order
   
   await link.approve(dex.address,200);
   await dex.addToken(web3.utils.fromUtf8("LINK"),link.address,{from: accounts[0]});
   await dex.deposit(100,web3.utils.fromUtf8("LINK"),{from: accounts[0]});

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

 });

 it("buyer needs to have enough eth for the trade",async ()=> {
   let dex = await Dex.deployed();

   await truffleAssert.reverts(dex.createMarketOrder(0,web3.utils.fromUtf8("LINK"),35)); //buy market order
   await dex.depositETH({value: 100000});
   await truffleAssert.passes(dex.createMarketOrder(0,web3.utils.fromUtf8("LINK"),35)); //buy market order

 });

it("market sell orders should be filled until the buy order book is empty or order is 100% filled",async () => {
    let dex = await Dex.deployed();
    await dex.depositETH({value: 1000,from: accounts[1]});
       
    await dex.createLimitOrder(0,web3.utils.fromUtf8("LINK"),10, 9,{from: accounts[1]}); //buy limit order

    await truffleAssert.passes(dex.createMarketOrder(1,web3.utils.fromUtf8("LINK"),35,{from: accounts[0]}));//sell market order
    const buyList = await dex.getOrderBook(web3.utils.fromUtf8("LINK"),0);
    assert(buyList.length == 0);

    var balance = await dex.balances(accounts[0],web3.utils.fromUtf8("LINK"));
    assert(balance == 100-10,"token balance of the seller should decrease with the filled amounts");
});

it("market buy order should be filled until the sell order book is empty or order is 100% filled",async () => {
    let dex = await Dex.deployed();
    let link = await Link.deployed();

    await link.mintTokens(accounts[3],2000);
    await link.approve(dex.address,1000,{from: accounts[3]});
    await dex.deposit(500,web3.utils.fromUtf8("LINK"),{from: accounts[3]});

    await dex.depositETH({value: 100000,from: accounts[2]});

    await dex.createLimitOrder(1,web3.utils.fromUtf8("LINK"),10, 13,{from: accounts[3]});//sell limit order

    await truffleAssert.passes(dex.createMarketOrder(0,web3.utils.fromUtf8("LINK"),35,{from: accounts[2]}));//buy market order
    const sellList = await dex.getOrderBook(web3.utils.fromUtf8("LINK"),1);
    assert(sellList.length == 0);
    
    var balance = await dex.balances(accounts[2],web3.utils.fromUtf8("ETH"));
    assert(balance == 100000 - 10*13,"ETH balance of the buyer should decrease with the filled amount");
});

});

createMarketOrder() of dex.sol

function createMarketOrder(Side side, bytes32 ticker, uint amount) public {
    
    if(Side.SELL == side){
        require(amount <= balances[msg.sender][ticker],"Insufficent tokens to sell");
        Order[] storage orders = orderBook[ticker][0]; //Buy order book
        uint i = 0; 
        while(amount > 0 && orders.length > 0){
         if(amount >= orders[i].amount){
             //updating the amount for seller
             Order memory temp = orders[i];
             amount -= temp.amount; //left amount
             balances[msg.sender]["ETH"] += (temp.price * temp.amount); // depositing eth in the account of seller
             balances[msg.sender][ticker] -= temp.amount; // withdrawing tickers from the account of seller
             balances[temp.trader]["ETH"] -= (temp.price * temp.amount); // reducing eth from the acccount of buyer
             balances[temp.trader][ticker] += temp.amount; //adding ticker to the account of buyer
             temp.amount = 0;
             orders[i] = temp;
             //deleting the fulfilled order from buy order book by shifting rows to the front
             for(uint j = 0 ; j < orders.length-1 ; j++){
                 orders[j] = orders[j+1];
             }
             orders.pop(); // removing the last duplicate row
             i++;
         }
         else if(amount < orders[i].amount){
             Order memory temp = orders[i];
             temp.amount -= amount;
             balances[msg.sender]["ETH"] += temp.price * amount; //seller
             balances[msg.sender][ticker] -= amount; //should become 0 //seller
             balances[temp.trader]["ETH"] -= temp.price * amount; //buyer
             balances[temp.trader][ticker] += amount; //buyer
             amount = 0;
             orders[i] = temp;
         }
     }
    }
    else if(Side.BUY == side){
        //make sure that buyer has enough eth to buy tokens
        Order[] storage orders = orderBook[ticker][1]; //limit order sell book to get maximum selling price
        if(orders.length > 0){
            require(balances[msg.sender]["ETH"] >= amount * orders [orders.length-1].price,"Insufficient ETH to buy");
        }
        require(balances[msg.sender]["ETH"] >= 100000,"Should keep your wallet heavy");

        uint i = 0;
        while(amount > 0 && orders.length > 0){
            if(amount >= orders[i].amount){
               Order memory temp = orders[i];
               amount -= temp.amount;
               balances[msg.sender][ticker] += temp.amount ;//buyer
               balances[msg.sender]["ETH"] -= temp.price * temp.amount; // buyer
               balances[temp.trader][ticker] -= temp.amount; // seller
               balances[temp.trader]["ETH"] += temp.price * temp.amount; //seller
               temp.amount = 0;
               orders[i] = temp;

               //deleting the fulfilled order from sell order book by shifting rows to the front
                for(uint j = 0 ; j < orders.length-1 ; j++){
                    orders[j] = orders[j+1];
                }
                orders.pop(); // removing the last duplicate row
                i++;
            }
            else if(amount < orders[i].amount){
                Order memory temp = orders[i];
                temp.amount -= amount;
                balances[msg.sender][ticker] += amount ;//buyer
                balances[msg.sender]["ETH"] -= temp.price * amount; // buyer
                balances[temp.trader][ticker] -= amount; // seller
                balances[temp.trader]["ETH"] += temp.price * amount; //seller
                amount = 0;
                orders[i] = temp;
            }
        } 
    }

   }
1 Like

hey @tanu_g. soo good that you have coded your own market order solutiion. Because this function is tricky and its so good to see you trying your own approach. Your tests seem fine and cover the nessecary points of intrest such as making sure buyer has enoigh eth etc all these things. I like that in your tests that you always tests boths sides ie you do a truffle reverts and then a truffle passes. You seeem to be very handy at coding so what i can suggest as a push is that you read the truffle docs (about testing) and there are ways you can make your tests more efficent. For example we deploy link and the dex in each test. To reduce these lines of code in each test we can use hooks like beforeEach() or beforeAll() where we define some code we want run before all of our tests for example to save us having to write duplicated code. But well done on the tests i only mention this as i think you would enjoy looking into it.

In terms of your actual script. I reallly dont want to critique because youve done such a good job and this is so completely awesome. However there are two things that i can say which mayyybee you could optimize in your code solution. The first is your require statement for the makret order Buy side

require(balances[msg.sender]["ETH"] >= 100000,"Should keep your wallet heavy");

I know why your doing this. The buy side of the market order is quite tricky because we dont specify a price as an argument in the market order function. The market order is satisfied at whatever price is currently at the top of the orderbook. This seems fine but a problem arises in the edge case where we have say 3 limit orders selling for example, LINKl @5 LINK each for say a price of 10, 8. 7 respectively. Then someone comes along and wants to make a market buy order for 15 LINK. This market order will settle the entire sell side of the orderbook but the price is not common therefore 1 require statment will not suffice.

I see what you have done here you just require that our eth balance is larger than some arbritrarily large number to ensure this edge case never happens. And this is absolutley fine for testing (as the main goal of the market order is not this small require) but if you really want to optimise your solution you need to fix this require.

Ok so this is what i propose. You have an orderbook selling LINK similar to above with three orders similar to above. Then we come along and make a market BUY to that will fill the entire sell side of the book. What if we make some var called uint amountFilled. This var will keep track of how much and order[ i ] fills of our market order. If the market orders “amount” is equal to or less than the sell sides “amount” then we know we just have to do a simple require once and the order has been settled no need to progres further in the orderbook. However if the market orders amount is grrater than the sell sides amount then we need to progress through the orderbook continuing to settle the market order, update balances and check if it has been filled. The below code below is my proposed solution to check if the user has enough balance of eth to cover the costs

else if(Side.BUY == side){

        uint amountFilled = 0;
        for (uint i = 0; i < orders.length; i++) {

                require(balance[msg.sender]["ETH"] > orders[i].price * amount
                uint amountFilled = _amount; ///_amount is the function argument the user passes in
                if (amountFilled <= orders[i].price) {
                       break;
                }
                else {
                       filledAmount = 0;
                }
        }
        ...........
        ...........
}

I have only come up with this code on the spot so it is not tested and may (most probablly) has some bugs. But it is the conecpt that is important and you can use this to fix your require issue by modifiying it for example. Basically my thinking is this. If we are creating a buy market order, when we enter the orderbook we want to check if we have enouugh ether to settle “THE CURRENT ORDER” in the orderBook. We can simply do this with

require(balance[msg.sender]["ETH"] > orders[i].price * amount

However this is not enough, we need to check if our market order has been 100% filled or not. Thus we create an amountFilled var that keeps track of this “per order”. We set it to 0 initially. Then we enter the orderbook for the first time. Since we have entered it we know that we have to have enough balance to cover the cost so we check. Then we set the amountFilled var equal to the amount we pass in to the function. We then check if this amountFilled var is less than or equal to the amount of the sell order. If it is then we know the order is satisfied and thus only required that one balance check which we did above. However if the amountFilled var is greater thanorders[i].price then we know the market order has not been filled and thus we need to proceed through the array and check the next element. Thus we need to do another require. What we can do is set the amountFilled back to 0 and then proceed as if its the first time we entered the array.

Anyways thats my thinking. There may be bugs but its an idea for you. My second thing to say is to do with nested loops. I know my fix above creates the need for a loop and with your current state of code it will make you have nested loops which is not desireable and if you use the aproach above you will have a while loop with 2 nested for loops. But there is a way to optimize this a little. I want you to try something. In your code make say 3 or 4 limit orders on the sell side. Then come in and create a huge market order that covers all 4 limit orders. Notice how long it takes for this to run. When i did this dex my market order solution was very similar to yours. I had an outer while loop and inside it i had a for loop. When i made a large order like this my code ran so slow took maybe 5 - 10s seconds for it to run. When i increased the amount of imit orders to like 5 or higher and makde a market order to settled them all my code crashed as the gas limit was exceeded. Thus i was forced back to the drawing board with my algorithm. My market solution has no nested loops after i optimised and doesnt crash woth this edge case that requires a lot of iteration.

You could could perhaps get rid of this for loop which yo have at the bottom which deletes settled orders

for(uint j = 0 ; j < orders.length-1 ; j++){
     orders[j] = orders[j+1];
}
orders.pop(); // removing the last duplicate row
i++;

What you could do is remove this and place the code into its own delete function that you could call after you create a market order. That way it will get rid of the nested loop situation. To do this you may want to create a bool filled attribute in your struct and set it to true when filled then using the loop above pop all orders with filled == true. Or perhaps you dont even need to put it into its own function. Sticking with that filled attribute then at the bottom of your market order function in th eplace of your looop above check if order[i] is filled and then pop. For example something like.

if (order[i].filled) {
      order[i] = order[oder.length - 1];
      order.pop();
 }

Anyways these are just some things you could (potentially) imporve. they should not be seen as nessecary. However sadly my suggestions still leave you with a nested loop (i.e the require check) but perhaps you could imporve on this yourself. This is such a long awnser i dont want you to think im giving criticsm im not al all your awnser is definitely wayyyy better than my attempt at this when was doing this course and not many ppl go out on their own to do their own algorithm so hats off trully. Im just very long winded in my explanation of thing hahaha.

2 Likes

Hi @mcgrane5,
Thank you so much for all your valuable inputs.
Taking these points into account, I will try to find a better solution. :relaxed:

Tanu

2 Likes

Hey @tanu_g. Yoir solution is very good as is these are only suggestions. Congratulations though you flew through this course i only remeber seeing last week your multisig solution in the 101 course.

3 Likes

Thanks @mcgrane5. I’ve realized that I made a few mistakes in the code. However, I can optimize the code taking your suggestions. Also, Idk if the idea is correct -

Wanted to make it a part of else statement which I forgot. This way I could set the min limit on ETH when the sell order book is empty.

Tanu

1 Like

Hey @tanu_g. Sorry im misreading the second part of your message. Do you mean you wanted ro make the require balance check as part of an if statement?

1 Like

On the buy-side, initially, I’ve written 2 require statements. Wanted to do something like this -

if(orders.length > 0){
            require(balances[msg.sender]["ETH"] >= amount * orders[orders.length-1].price,"Insufficient ETH to buy");
        }
        else{
            require(balances[msg.sender]["ETH"] >= 100000,"Should keep your wallet heavy");
        }

For if statement I can use your suggestion like i can traverse the order list and all but not sure for the else one.
Hope this is clear now … :nerd_face:

1 Like

Yeah. I see what you mean now. The code i proposed above is to replace your if and else require statement. Id like to play with your code maybe. Because the “'keep your wallet hwavy” statement will prevent smaller users from making market buy orders. Could u push your code to github. Id like to try and improve my proposal for the eth balance requierment above. I want to see if incan do it wothout a second loop maybe. But i can only do so by playing with what u already have.

1 Like

@mcgrane5
Hey Evan,

Here - Link for DEX

Thanks,
Tanu

1 Like

Hey Evan,

If I follow the below approach, I may need a loop to sort the orders, which brings the loop back in the story.
Please correct me if I am in error.

1 Like

Hi all,

are files .js for test considered as Javascript or NodeJS? I thought it was NodeJS, however GitHub says something different. Thanks.

Tom

1 Like

.js is a javascript file.
NodeJs is a runtime environment that runs javascript outside the browser.

1 Like

Thanks. Okay, so NodeJS is in this case Visual Studio Code with Truffle. Right?

I’m currently working through the “Better migration” lesson and the javascript syntax is becoming more and more confusing to me - such as when to use await/async. When googling for an explanation, it let me down a rabbit hole on trying to figure out what promises are.

In the order of the courses I am recommended to take, the javascript course comes after this one… Would you recommend I do the javascript course first and then come back to complete this dex project? @dan-i

Indeed sir!

The JavaScript course should be taken first before starting the Ethereum Smart Contract Programming 201, there are some lessons on that course that will teach you how promises works and some examples to work with :nerd_face:

Carlos z

1 Like

Why it doesn’t want show me a test-cases inside it()? Why it says 0 passing?

walletTest.js

const Dex = artifacts.require("Dex")
const LINK = artifacts.require("LINK")
const truffleAssertions = require('truffle-assertions');

//First segment is calling function contract(1. argument - name of contract, 2nd - function anonymus i tu definiramo testove)
contract("Dex", accounts => {
    it("should only be possible for owner to add tokens", async () => {
        let dex = await Dex.deployed()
        let link = await LINK.deployed()
        await truffleAssertions.passes(dex.addToken(web3.utils.fromUtf8("LINK"), link.address, {from:accounts[0]}))
    })
    it("should handle deposits corretcly", async () => {
        let dex = await Dex.deployed()
        let link = await LINK.deployed()
        await link.approve(dex.address, 500)
        await dex.deposit(web3.utils.fromUtf8("LINK"), 100)
        let balance = dex.balances(accounts[0], web3.utils.fromUtf8("LINK"))
        assert.equal( balance.toNumber(), 100 )
        
    })
})

3_token_migration.js

const LINK = artifacts.require("LINK");
const Dex = artifacts.require("Dex");

module.exports = async function (deployer, network, accounts) {
  await deployer.deploy(LINK);
  let dex = await Dex.deployed()
  let link = await LINK.deployed()
  await link.approve(dex.address, 500);
  await dex.addToken(web3.utils.fromUtf8("LINK"), link.address)
  await dex.deposit(web3.utils.fromUtf8("LINK"), 100);
};

image

1 Like

Hey @Lane11

Push this one to github and share the repo please.

cheers,
Dani

1 Like

Hey @Zaqoy

The javascript course should be the 1st one to take imo.
It teaches you the basic of programming and also concepts like async functions (which is what you struggle with it see).

Long story short, there are functions that are executed “immediately” and functions that take some time.

As general rule (just to give you a path to follow), functions that do NOT interact with “the outside” are executed immediately:

ex:

const result = 1 + 1
console.log(result)

Functions that interact with the outside are slower, therefore you need to tell your code to stop the execution until the outside program responds.

ex:

const result = // ask the weather forecast to an external website
console.log(result)

In the example above, the function is asynchronous (takes time to execute) because you need to wait the external website/ server to answer your query.