Project - DEX Final Code

Hey folks :slightly_smiling_face:,

Here is part of DEX:

Wallet.sol
// SPDX-License-Identifier: MIT
pragma solidity > 0.6.0 <= 0.8.13;

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

contract Wallet is Ownable{
    using SafeMath for uint256;

    struct Token {
        bytes32 ticker;
        address tokenAddress;
    }

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

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

    modifier tokenExists(bytes32 ticker) {
        require(tokenMapping[ticker].tokenAddress != address(0));
        _;
    }

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

    function deposit(uint256 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);
    }

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

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

    function withdraw(uint256 amount, bytes32 ticker) tokenExists(ticker) external {
        require(balances[msg.sender][ticker] >= amount);

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

    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}("");
    }
}

Dex.sol
// SPDX-License-Identifier: MIT
pragma solidity > 0.6.0 <= 0.8.13;
pragma experimental ABIEncoderV2;

import "./Wallet.sol";

contract Dex is Wallet{
    using SafeMath for uint256;

    enum Side {
        BUY,
        SELL
    }

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

    uint public nextOrderId = 0;

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

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

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

        Order[] storage orders = orderBook[ticker][uint(side)];
        orders.push(Order(nextOrderId++, msg.sender, side, ticker, amount, price, 0));
        if (orders.length == 1) {
            return;
        }
        _sort(orders, side);
    }

    function createMarketOrder(Side side, bytes32 ticker, uint amount) public {
        if (side == Side.SELL) {
            require(balances[msg.sender][ticker] >= amount, "Not enough token for selling.");
        }
        Order[] storage orders = orderBook[ticker][uint(side == Side.BUY ? Side.SELL : Side.BUY)];
        uint totalFilled;
        for(uint i = 0; i < orders.length && totalFilled < amount; i++) {
            Order storage currentOrder = orders[i];
            if (currentOrder.amount > amount) {
                require(side == Side.BUY && balances[msg.sender][bytes32("ETH")] >= currentOrder.amount * currentOrder.price, "Not enough token to buy");
                currentOrder.filled = currentOrder.filled.add(amount);
                _trade(side, ticker, currentOrder);
                break;
            }
            require(side == Side.BUY && balances[msg.sender][bytes32("ETH")] >= currentOrder.amount * currentOrder.price, "Not enough token to buy");
            currentOrder.filled = currentOrder.amount;
            totalFilled = totalFilled.add(currentOrder.amount);
            _trade(side, ticker, currentOrder);
        }

        _removeFilledOrders(orders);
    }

    function _trade(Side side, bytes32 ticker, Order storage currentOrder) internal {
        if (side == Side.BUY) {
            balances[msg.sender][ticker] = balances[msg.sender][ticker].add(currentOrder.amount);
            balances[msg.sender][bytes32("ETH")] = balances[msg.sender][bytes32("ETH")].sub(currentOrder.amount * currentOrder.price);

            balances[currentOrder.trader][ticker] = balances[currentOrder.trader][ticker].sub(currentOrder.amount);
            balances[currentOrder.trader][bytes32("ETH")] = balances[currentOrder.trader][bytes32("ETH")].add(currentOrder.amount * currentOrder.price);
        } else {
            balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(currentOrder.amount);
            balances[msg.sender][bytes32("ETH")] = balances[msg.sender][bytes32("ETH")].add(currentOrder.amount * currentOrder.price);

            balances[currentOrder.trader][ticker] = balances[currentOrder.trader][ticker].add(currentOrder.amount);
            balances[currentOrder.trader][bytes32("ETH")] = balances[currentOrder.trader][bytes32("ETH")].sub(currentOrder.amount * currentOrder.price);
        }
    }

    function _removeFilledOrders(Order[] storage orders) internal {
        while(orders.length > 0 && orders[0].filled == orders[0].amount){
            for (uint256 i = 0; i < orders.length - 1; i++) {
                orders[i] = orders[i + 1];
            }
            orders.pop();
        }
    }

    function _sort(Order[] storage orders, Side side) internal {
        Order memory orderToMove;
        for (uint i = orders.length - 1; i > 0; i--) {
            bool buySideCondition = side == Side.BUY && orders[i].price > orders[i-1].price;
            bool sellSideCondition = side == Side.SELL && orders[i].price < orders[i-1].price;
            if (buySideCondition || sellSideCondition) {
                orderToMove = orders[i-1];
                orders[i-1] = orders[i];
                orders[i] = orderToMove;
                continue;
            }
        }
    }
}
dex_test.js
const Dex = artifacts.require('Dex');
const Link = artifacts.require('Link');
const truffleAssert = require('truffle-assertions');

contract('Dex', (accounts) => {
    describe('Limit orders', () => {
        it('should throw an error if ETH balance is too low when creating BUY limit order', async () => {
            const dex = await Dex.new();
            await truffleAssert.reverts(
                dex.createLimitOrder(0, web3.utils.fromUtf8('LINK'), 10, 1)
            );
            await dex.depositEth({ value: 10 });
            await truffleAssert.passes(
                dex.createLimitOrder(0, web3.utils.fromUtf8('LINK'), 10, 1)
            );
        });
        it('should throw an error if token balance is too low when creating SELL limit order', async () => {
            const dex = await Dex.new();
            const link = await Link.new();
            await truffleAssert.reverts(
                dex.createLimitOrder(1, web3.utils.fromUtf8('LINK'), 10, 1)
            );
            await dex.addToken(web3.utils.fromUtf8('LINK'), link.address);
            await link.approve(dex.address, 500);
            await dex.deposit(10, web3.utils.fromUtf8('LINK'));
            await truffleAssert.passes(
                dex.createLimitOrder(1, web3.utils.fromUtf8('LINK'), 10, 1)
            );
        });
        it('The BUY order book should be ordered on price from highest to lowest starting at index 0', async () => {
            const dex = await Dex.new();
            await dex.depositEth({ value: 600 });
            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);

            const orderBook = await dex.getOrderBook(
                web3.utils.fromUtf8('LINK'),
                0
            );
            assert(orderBook.length > 0);
            for (let i = 0; i < orderBook.length - 1; i++) {
                assert(orderBook[i].price >= orderBook[i + 1]?.price);
            }
        });
        it('The SELL order book should be ordered on price from lowest to highest starting at index 0', async () => {
            const dex = await Dex.new();
            const link = await Link.new();
            await dex.addToken(web3.utils.fromUtf8('LINK'), link.address);
            await link.approve(dex.address, 600);
            await dex.deposit(600, web3.utils.fromUtf8('LINK'));
            await dex.createLimitOrder(1, web3.utils.fromUtf8('LINK'), 1, 300);
            await dex.createLimitOrder(1, web3.utils.fromUtf8('LINK'), 1, 100);
            await dex.createLimitOrder(1, web3.utils.fromUtf8('LINK'), 1, 200);

            const orderBook = await dex.getOrderBook(
                web3.utils.fromUtf8('LINK'),
                1
            );
            assert(orderBook.length > 0);
            for (let i = 0; i < orderBook.length - 1; i++) {
                assert(orderBook[i].price <= orderBook[i + 1]?.price);
            }
        });
    });

    describe('Martet orders:', () => {
        it('Should throw an error when creating a sell market order without adequate token balance', async () => {
            const dex = await Dex.new();

            const balance = await dex.balances(
                accounts[0],
                web3.utils.fromUtf8('LINK')
            );
            assert.equal(
                balance.toNumber(),
                0,
                'Initial LINK balance is not 0'
            );

            await truffleAssert.reverts(
                dex.createMarketOrder(1, web3.utils.fromUtf8('LINK'), 10)
            );
        });
        it('Market orders can be submitted even if the order book is empty', async () => {
            const dex = await Dex.new();

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

            const buyOrderBook = await dex.getOrderBook(
                web3.utils.fromUtf8('LINK'),
                0
            );
            const sellOrderBook = await dex.getOrderBook(
                web3.utils.fromUtf8('LINK'),
                0
            );
            assert(
                buyOrderBook.length == 0,
                'Buy side Orderbook length is not 0'
            );
            assert(
                sellOrderBook.length == 0,
                'Buy side Orderbook length is not 0'
            );

            await truffleAssert.passes(
                dex.createMarketOrder(0, web3.utils.fromUtf8('LINK'), 10)
            );
        });
        it('Market orders should not fill more limit orders than the market order amount', async () => {
            const dex = await Dex.new();

            let orderBook = await dex.getOrderBook(
                web3.utils.fromUtf8('LINK'),
                1
            );
            assert(
                orderBook.length == 0,
                'Sell side Orderbook should be empty at start of test'
            );

            await depositLinkToAccounts(dex, accounts);

            await dex.createLimitOrder(1, web3.utils.fromUtf8('LINK'), 5, 300, {
                from: accounts[1],
            });
            await dex.createLimitOrder(1, web3.utils.fromUtf8('LINK'), 5, 400, {
                from: accounts[2],
            });
            await dex.createLimitOrder(1, web3.utils.fromUtf8('LINK'), 5, 500, {
                from: accounts[3],
            });

            await truffleAssert.reverts(
                dex.createMarketOrder(0, web3.utils.fromUtf8('LINK'), 10)
            );
            await dex.depositEth({ value: 10000 });
            orderBook = await dex.getOrderBook(web3.utils.fromUtf8('LINK'), 1);
            await truffleAssert.passes(
                dex.createMarketOrder(0, web3.utils.fromUtf8('LINK'), 10)
            );

            orderBook = await dex.getOrderBook(web3.utils.fromUtf8('LINK'), 1); //Get sell side orderBook
            assert(
                orderBook.length == 1,
                'Sell side Orderbook should only have 1 order left'
            );
            assert(
                orderBook[0].filled == 0,
                'Sell side order should have 0 filled'
            );
        });
        it('Market orders should be filled until the order book is empty', async () => {
            const dex = await Dex.new();

            const orderBook = await dex.getOrderBook(
                web3.utils.fromUtf8('LINK'),
                1
            );
            assert(
                orderBook.length == 0,
                'Sell side Orderbook should be empty at start of test'
            );

            await depositLinkToAccounts(dex, accounts);

            await dex.createLimitOrder(1, web3.utils.fromUtf8('LINK'), 5, 500, {
                from: accounts[3],
            });
            await dex.createLimitOrder(1, web3.utils.fromUtf8('LINK'), 5, 400, {
                from: accounts[1],
            });
            await dex.createLimitOrder(1, web3.utils.fromUtf8('LINK'), 5, 500, {
                from: accounts[2],
            });

            const balanceBefore = await dex.balances(
                accounts[0],
                web3.utils.fromUtf8('LINK')
            );

            await truffleAssert.reverts(
                dex.createMarketOrder(0, web3.utils.fromUtf8('LINK'), 50)
            );
            await dex.depositEth({ value: 10000 });
            await dex.createMarketOrder(0, web3.utils.fromUtf8('LINK'), 50);

            const balanceAfter = await dex.balances(
                accounts[0],
                web3.utils.fromUtf8('LINK')
            );

            assert.equal(
                balanceBefore.toNumber() + 15,
                balanceAfter.toNumber()
            );
        });

        it('The eth balance of the buyer should decrease with the filled amount', async () => {
            const dex = await Dex.new();

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

            await dex.depositEth({ value: 10000 });
            const balanceBefore = await dex.balances(
                accounts[0],
                web3.utils.fromUtf8('ETH')
            );
            await dex.createMarketOrder(0, web3.utils.fromUtf8('LINK'), 1);
            const balanceAfter = await dex.balances(
                accounts[0],
                web3.utils.fromUtf8('ETH')
            );

            assert.equal(
                balanceBefore.toNumber() - 300,
                balanceAfter.toNumber()
            );
        });

        it('The token balances of the limit order sellers should decrease with the filled amounts.', async () => {
            const dex = await Dex.new();

            const orderBook = await dex.getOrderBook(
                web3.utils.fromUtf8('LINK'),
                1
            );
            assert(
                orderBook.length == 0,
                'Sell side Orderbook should be empty at start of test'
            );

            await depositLinkToAccounts(dex, accounts);

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

            const account1balanceBefore = await dex.balances(
                accounts[1],
                web3.utils.fromUtf8('LINK')
            );
            const account2balanceBefore = await dex.balances(
                accounts[2],
                web3.utils.fromUtf8('LINK')
            );

            await dex.depositEth({ value: 10000 });
            await dex.createMarketOrder(0, web3.utils.fromUtf8('LINK'), 2);

            const account1balanceAfter = await dex.balances(
                accounts[1],
                web3.utils.fromUtf8('LINK')
            );
            const account2balanceAfter = await dex.balances(
                accounts[2],
                web3.utils.fromUtf8('LINK')
            );

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

        it('Filled limit orders should be removed from the orderBook', async () => {
            const dex = await Dex.new();
            const link = await Link.new();
            await dex.addToken(web3.utils.fromUtf8('LINK'), link.address);

            await link.approve(dex.address, 500);
            await dex.deposit(50, web3.utils.fromUtf8('LINK'));

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

            let orderBook = await dex.getOrderBook(
                web3.utils.fromUtf8('LINK'),
                1
            );

            await dex.createLimitOrder(1, web3.utils.fromUtf8('LINK'), 1, 300);
            await dex.createMarketOrder(0, web3.utils.fromUtf8('LINK'), 1);

            orderBook = await dex.getOrderBook(web3.utils.fromUtf8('LINK'), 1);
            assert(
                orderBook.length == 0,
                'Sell side Orderbook should be empty after trade'
            );
        });

        it('Limit orders filled property should be set correctly after a trade', async () => {
            const dex = await Dex.new();

            let orderBook = await dex.getOrderBook(
                web3.utils.fromUtf8('LINK'),
                1
            );
            assert(
                orderBook.length == 0,
                'Sell side Orderbook should be empty at start of test'
            );

            await depositLinkToAccounts(dex, accounts);

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

            orderBook = await dex.getOrderBook(web3.utils.fromUtf8('LINK'), 1);
            assert.equal(orderBook[0].filled, 2);
            assert.equal(orderBook[0].amount, 5);
        });
        it('Should throw an error when creating a buy market order without adequate ETH balance', async () => {
            const dex = await Dex.new();

            const balance = await dex.balances(
                accounts[4],
                web3.utils.fromUtf8('ETH')
            );
            assert.equal(balance.toNumber(), 0, 'Initial ETH balance is not 0');

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

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

// helper functions
/**
 *
 * @param {Dex} dex
 * @param {Account[]} accounts
 */
async function depositLinkToAccounts(dex = {}, accounts = []) {
    const link = await Link.new();
    await dex.addToken(web3.utils.fromUtf8('LINK'), link.address);

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

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

    await dex.deposit(50, web3.utils.fromUtf8('LINK'), {
        from: accounts[1],
    });
    await dex.deposit(50, web3.utils.fromUtf8('LINK'), {
        from: accounts[2],
    });
    await dex.deposit(50, web3.utils.fromUtf8('LINK'), {
        from: accounts[3],
    });
}

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

contract('Dex', (accounts) => {
    it('should only be possible for owner to add tokens', async () => {
        let dex = await Dex.deployed();
        let link = await Link.deployed();
        let linkNameInBytes = web3.utils.fromUtf8('LINK');

        await truffleAssert.reverts(
            dex.addToken(linkNameInBytes, link.address, { from: accounts[1] })
        );
        await truffleAssert.passes(
            dex.addToken(linkNameInBytes, link.address, { from: accounts[0] })
        );
    });
    it('should deposits correctly', async () => {
        const dex = await Dex.deployed();
        const link = await Link.deployed();
        const linkNameInBytes = web3.utils.fromUtf8('LINK');

        await link.approve(dex.address, 500);
        await dex.deposit(100, linkNameInBytes);

        const resultBalance = await dex.balances(accounts[0], linkNameInBytes);
        assert.equal(resultBalance.toNumber(), 100);
    });
    it('should handle faulty withdrawals correctly', async () => {
        const dex = await Dex.deployed();
        const linkNameInBytes = web3.utils.fromUtf8('LINK');

        await truffleAssert.reverts(dex.withdraw(500, linkNameInBytes));
        await truffleAssert.passes(dex.withdraw(100, linkNameInBytes));
    });
});

1 Like

thanks brother its help alot

hey @bjamRez this is really brillliant work. can u write another post explaining some of the userEffect re-render issues that your facing. i might be able to propose a solution if you can give a detailed post. because there are techniques you can employ to stop these re-redners

1 Like

yeah these types of errors are really difficult to debug. i would suggest trying to delete your node modules . package.json and package lock.json and running mpm init again from scratch. ive had to do this before in project and it works fine

hey @mcgrane5 thanks for checking out my semi-final DEX proj. Yes - I can do detailed post - I will be busy in next few days but I’ll see if I can get it done post it here.

1 Like
wallet.sol
pragma solidity ^0.8.0;

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

contract Wallet is Ownable {
    using SafeMath for uint256;
    
    struct Token {
        bytes32 ticker;
        address tokenAddress;
    }
    
    modifier tokenExist(bytes32 ticker) {
        require(tokenMapping[ticker].tokenAddress != address(0), 'token does not exist');
        _;
    }
    
    mapping(bytes32 => Token) public tokenMapping;
    bytes32[] public tokenList;
    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,'Insuffient balance'); 
        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};
    }

}
dex.sol
pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;

import "./wallet.sol";

contract Dex is Wallet {
    using SafeMath for uint256;

    enum Side {
        BUY,
        SELL
    }

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

    uint public nextOrderId = 0;

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

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

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

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

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

        if(side == Side.BUY){
            while(i>0){
                if(orders[i-1].price>orders[i].price){
                    break;
                }
                (orders[i], orders[i-1]) = (orders[i-1], orders[i]);
                i--;
            }
        }else {
            while(i>0){
                if(orders[i-1].price < orders[i].price){
                    break;
                }
                (orders[i], orders[i-1]) = (orders[i-1], orders[i]);
                i--;
            }
        }

        nextOrderId++;
    }

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

        bool totalFilled = false;

        for (uint256 i = 0; i < orders.length && totalFilled == false; i++) {
            uint filled = 0;
            if(orders[i].amount > amount){
                orders[i].amount = orders[i].amount.sub(amount);
                filled = amount;
            }
            else{
                orders[i].filled = true;
                filled = orders[i].amount;
                orders[i].amount = 0;
            }
            amount = amount.sub(filled);
            totalFilled = amount == 0 ? true : false;
            if(side == Side.BUY){
                require(balances[msg.sender]["ETH"] >= orders[i].price.mul(filled));
                balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].sub(orders[i].price.mul(filled));
                balances[orders[i].trader]["ETH"] = balances[orders[i].trader]["ETH"].add(orders[i].price.mul(filled));
                balances[msg.sender][ticker] = balances[msg.sender][ticker].add(filled);
                balances[orders[i].trader][ticker] = balances[orders[i].trader][ticker].sub(filled);
            }else{
                balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].add(orders[i].price.mul(filled));
                balances[orders[i].trader]["ETH"] = balances[orders[i].trader]["ETH"].sub(orders[i].price.mul(filled));
                balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(filled);
                balances[orders[i].trader][ticker] = balances[orders[i].trader][ticker].add(filled);
            } 
        }

        while(orders.length>0 && orders[0].filled == true){
            for(uint i=0; i<orders.length-1;i++){
                orders[i] = orders[i+1];
            }
            orders.pop();
        }

    }
    
} 

for dex.sol I get a warning for this line:

(orders[i], orders[i-1]) = (orders[i-1], orders[i]);

Warning: This assignment performs two copies to storage. Since storage copies do not first copy to a temporary location, one of them might be overwritten before the second is executed and thus may have unexpected effects. It is safer to perform the copies separately or assign to storage pointers first.

Should I change to how flip did it in his code or is it ok to leave it like this code passes all the tests.

1 Like

yeah, although i cant remeber if Filp does this inthe course. but good catch there amazing work. also huge congrats on finishing the course

Hello @thecil I’m writing a couple of tests for my contract and I thought it would be cool to test that the events are being emitted correctly but I got stuck. This is my code:
DEX.sol

event LimitOrderCreated(bytes32 ticker, Side side, uint amount, uint price);
    function createLimitOrder(bytes32 _ticker, Side _side, uint _amount, uint _price ) tokenExist(_ticker) external{
        if(_side == Side.BUY){
            require(balances[_msgSender()]["ETH"] >= SafeMath.mul(_amount, _price), "Sender doesn't have enougth ETH to buy"); 
        }
        if(_side == Side.SELL){
            require(balances[_msgSender()][_ticker] >= _amount, "Sender doesn't have enougth TOKEN to sell");
        }
        Order memory order = Order(Counters.current(counter), _msgSender(), _side, _ticker, _amount, _price);
        
        orderBook[_ticker][uint(_side)].push(order);
        
        sortArray(orderBook[_ticker][uint(_side)], _side);
        emit LimitOrderCreated(_ticker, _side, _amount, _price);
    }

    function sortArray(Order[] storage orders, Side _side) private {
        for(uint i=0; i< orders.length - 1; i++){
            for(uint j=0 ; j< orders.length- i -1 ; j++){
                if (Side.BUY == _side && orders[j].price < orders[j+1].price){
                    Order storage aux = orders[j+1];
                    orders[j+1] = orders[j];
                    orders[j] = aux;
                }
                if (Side.SELL == _side && orders[j].price > orders[j+1].price){
                    Order storage aux = orders[j+1];
                    orders[j+1] = orders[j];
                    orders[j] = aux;
                }
            }
        }
    }

and this is how I’m testing for it:

it("createLimitOrder should throw  limit order created with the operation data", async () => {
        let dex = await Dex.deployed();
        createLimitOrder = await dex.createLimitOrder(USDT_TICKER, SELL, 1, PRICE+10 , {from: accounts[1]});
        
        truffleAssert.eventEmitted(createLimitOrder, 'LimitOrderCreated', (ev) => {
            return ev.ticker == USDT_TICKER && ev.side == SELL && ev.price.toNumber() === PRICE+10 && ev.amount.toNumber() ===1;
        }, 'TestEvent should be emitted with correct parameters');
        
    })

This is the error I’m getting:

1) Contract: Dex
       createLimitOrder should throw  limit order created with the operation data:
     AssertionError: TestEvent should be emitted with correct parameters : Event filter for LimitOrderCreated returned no results
Events emitted in tx 0xa1291e578c3ea168899952d865ba12e36a78015db787a302ce26f60d97f5af3d:
----------------------------------------------------------------------------------------
LimitOrderCreated(0: 0x5553445400000000000000000000000000000000000000000000000000000000, 1: 1, 2: 1, 3: 11, __length__: 4, ticker: 0x5553445400000000000000000000000000000000000000000000000000000000, side: 1, amount: 1, price: 11)
----------------------------------------------------------------------------------------

I don’t really understand what the error is not how can I solve it

Thanks in advance
Ale

I have updated my dex.sol file but the issue with the events is still happenning

function createLimitOrder(bytes32 _ticker, Side _side, uint _amount, uint _price ) tokenExist(_ticker) external{
        if(_side == Side.BUY){
            require(balances[_msgSender()]["ETH"] >= SafeMath.mul(_amount, _price), "Sender doesn't have enougth ETH to buy"); 
        }
        else if(_side == Side.SELL){
            require(balances[_msgSender()][_ticker] >= _amount, "Sender doesn't have enougth TOKEN to sell");
        }
        Order[] storage orders = orderBook[_ticker][uint(_side)];
        
        orders.push(Order(Counters.current(counter), _msgSender(), _side, _ticker, _amount, _price));
        emit LimitOrderCreated(_ticker, _side, _amount, _price);
        sortArray(orderBook[_ticker][uint(_side)], _side);
        Counters.increment(counter);
    }
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
pragma abicoder v2;

import "./Wallet.sol";

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

contract Dex is Wallet {
    using SafeMath for uint256;

    enum Side {
        BUY,
        SELL
    }

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

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

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

    function createLimitOrder(
        Side side,
        bytes32 ticker,
        uint256 amount,
        uint256 price
    ) public {
        if (side == Side.BUY) {
            require(
                balances[msg.sender]["ETH"] >= amount * price,
                "Balance too low..."
            );
        } else if (side == Side.SELL) {
            require(
                balances[msg.sender][ticker] >= amount,
                "Amount too low..."
            );
        }

        require(tokenMapping[ticker].ticker == ticker, "Token doesn't exist");

        uint256 orderId = getOrderBook(ticker, side).length;
        Order memory order = Order(
            orderId,
            msg.sender,
            side,
            ticker,
            amount,
            price,
            0
        );

        orderBook[ticker][uint256(side)].push(order);

        //SORT
        if (uint256(side) == 1) {
            //Sell
            uint256 len = (orderBook[ticker][uint256(side)]).length;

            for (uint256 i = 0; i < len - 1; i++) {
                for (uint256 j = 0; j < len - 1; j++) {
                    if (
                        (orderBook[ticker][uint256(side)])[j].price >
                        (orderBook[ticker][uint256(side)])[j + 1].price
                    ) {
                        Order memory temp = (orderBook[ticker][uint256(side)])[
                            j
                        ];
                        (orderBook[ticker][uint256(side)])[j] = (
                            orderBook[ticker][uint256(side)]
                        )[j + 1];
                        (orderBook[ticker][uint256(side)])[j + 1] = temp;
                    }
                }
            }
        } else if (uint256(side) == 0) {
            // Buy
            uint256 len = (orderBook[ticker][uint256(side)]).length;
            for (uint256 i = 0; i < len - 1; i++) {
                for (uint256 j = 0; j < len - 1; j++) {
                    if (
                        (orderBook[ticker][uint256(side)])[j].price <
                        (orderBook[ticker][uint256(side)])[j + 1].price
                    ) {
                        Order memory temp = (orderBook[ticker][uint256(side)])[
                            j
                        ];
                        (orderBook[ticker][uint256(side)])[j] = (
                            orderBook[ticker][uint256(side)]
                        )[j + 1];
                        (orderBook[ticker][uint256(side)])[j + 1] = temp;
                    }
                }
            }
        }
    }

    function createMarketOrder(
        Side side,
        bytes32 ticker,
        uint256 amount
    ) public {
        uint256 orderBookSide;
        if (side == Side.BUY) {
            orderBookSide = 1;
        } else {
            require(
                balances[msg.sender][ticker] >= amount,
                "Insufficent amount"
            );
            orderBookSide = 0;
        }

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

        uint256 totalFilled;

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

            totalFilled = totalFilled.add(filled);
            orders[i].filled = orders[i].filled.add(filled);
            uint256 cost = filled.mul(orders[i].price);

            if (side == Side.BUY) {
                // Verify that the buyer has enough ETH
                require(
                    balances[msg.sender]["ETH"] >= filled.mul(orders[i].price)
                );
                // Transfer ETH
                transfer(msg.sender, orders[i].trader, cost, "ETH");
                // Transfer the token
                transfer(orders[i].trader, msg.sender, filled, ticker);
            } else if (side == Side.SELL) {
                transfer(orders[i].trader, msg.sender, cost, "ETH");
                // Transfer the token
                transfer(msg.sender, orders[i].trader, filled, ticker);
            }
        }
        // Loop through orderbook and remove 100% filled orders
        while (orders[0].filled == orders[0].amount && orders.length > 0) {
            for (uint256 i = 0; i < orders.length - 1; i++) {
                orders[i] = orders[i + 1];
            }
            orders.pop();
        }
    }
}

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

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

contract Wallet is Ownable {
    using SafeMath for uint256;

    struct Token {
        bytes32 ticker;
        address tokenAddress;
    }

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

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

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

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

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

    function withdraw(uint256 amount, bytes32 ticker)
        external
        tokenExist(ticker)
    {
        require(
            balances[msg.sender][ticker] >= amount,
            "Balance not sufficient"
        );
        balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(amount);
        IERC20(tokenMapping[ticker].tokenAddress).transfer(msg.sender, amount);
    }

    function getBalance(address _address, bytes32 ticker)
        public
        view
        returns (uint256)
    {
        return balances[_address][ticker];
    }

    function transfer(
        address _from,
        address _to,
        uint256 amount,
        bytes32 ticker
    ) public {
        require(balances[_from][ticker] >= amount);
        balances[_to][ticker].add(amount);
        balances[_from][ticker].sub(amount);
    }
}

1 Like

hmmm not too sure on this one but in solidity you cant compare strings u will need to hash the string and compare the hashes. suing keecak256. maybe this is why its failing

Hello @filip and @thecil,
I have created my own tests and also changed the shift and delete mechanisms
These are my files
Wallet.sol

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

import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract Wallet is AccessControl, ReentrancyGuard{
    using Counters for Counters.Counter;
    using SafeMath for uint256;

    bytes32 public constant TOKEN_REGISTRATOR_ROLE = keccak256("TOKEN_REGISTRATOR_ROLE");
    bytes32 public constant TOKEN_REGISTRATOR_ADMIN_ROLE = keccak256("TOKEN_REGISTRATOR_ADMIN_ROLE");
    bytes32 public constant CONTRACT_DESTROYER_ROLE = keccak256("CONTRACT_DESTROYER_ROLE");

    // maps address to token-ticker to amounts
    // usa bytes32 en lugar de string por que en solidity no se pueden comparar strings
    mapping(address => mapping(bytes32 => uint256)) public balances;

    struct Token{
        bytes32 ticker;
        address tokenAddress;
    }

    bytes32[] public tokenList; // List with all the tickers registered -- ability to iterate thru all the tokens
    mapping(bytes32 => Token) public tokenMapping; //for each ticker get the token 
    
    event TokenRegisteredEvent(bytes32 indexed ticker, address indexed tokenAddress);
    event TokenDepositApproved(bytes32 indexed ticker, uint amount, address account);
    event TokenDepositRejected(bytes32 indexed ticker, uint amount, address account);
    event TokenDepositDone(bytes32 indexed ticker, uint amount, address account);

    event DebugBalanceOf(bytes32 indexed ticker, uint amount, address account);

    modifier tokenExist(bytes32 token){
        require(tokenMapping[token].tokenAddress != address(0), "Token should be registered first");
        _;
    }

    constructor(){
        _grantRole(TOKEN_REGISTRATOR_ADMIN_ROLE, _msgSender());
        _grantRole(TOKEN_REGISTRATOR_ROLE, _msgSender());
        _grantRole(DEFAULT_ADMIN_ROLE, _msgSender());
        _setRoleAdmin(TOKEN_REGISTRATOR_ROLE, TOKEN_REGISTRATOR_ADMIN_ROLE);
        _grantRole(CONTRACT_DESTROYER_ROLE, _msgSender());
    }

    function balanceOf(bytes32 _ticker, address user) public returns (uint256){
        emit DebugBalanceOf(_ticker, balances[user][_ticker] , user);
        return balances[user][_ticker];
    }

    function registerToken(bytes32 _ticker, address _tokenAddress) external onlyRole(TOKEN_REGISTRATOR_ROLE){
        tokenMapping[_ticker] =Token(_ticker, _tokenAddress);
        tokenList.push(_ticker);
        emit TokenRegisteredEvent(_ticker, _tokenAddress);
    }

    function deposit(uint _amount, bytes32 _ticker) tokenExist(_ticker) external {
        
        balances[msg.sender][_ticker] = SafeMath.add(balances[msg.sender][_ticker], _amount);
        IERC20 token = IERC20(tokenMapping[_ticker].tokenAddress);
        uint allowence = token.allowance(_msgSender(), address(this)); 
        if (allowence >= _amount){
            emit TokenDepositApproved(_ticker, _amount, msg.sender);
            IERC20(tokenMapping[_ticker].tokenAddress).transferFrom(msg.sender, address(this), _amount);
            emit TokenDepositDone(_ticker, _amount, msg.sender);
        }else{
            emit TokenDepositRejected(_ticker, _amount, msg.sender);
            balances[msg.sender][_ticker] = SafeMath.sub(balances[msg.sender][_ticker], _amount);
        }
        
    }
    function depositEth() payable external {
        balances[msg.sender][bytes32("ETH")] = SafeMath.add(balances[msg.sender][bytes32("ETH")], msg.value);
    }

    function withdrawEth(uint _amount) payable external nonReentrant {
        require(balances[msg.sender][bytes32("ETH")] > _amount, "Insufficient balance");
        balances[msg.sender][bytes32("ETH")] = SafeMath.sub(balances[msg.sender][bytes32("ETH")], msg.value);
        msg.sender.call{value:_amount}("");
    }

    function withdraw(uint _amount, bytes32 _ticker) tokenExist(_ticker) external{
        require( balances[msg.sender][_ticker] >= _amount, "balance should be bigger thant the amount");

        balances[msg.sender][_ticker] = SafeMath.sub(balances[msg.sender][_ticker], _amount);
        IERC20(tokenMapping[_ticker].tokenAddress).transfer(msg.sender, _amount);
    }
    
    function destroy()  onlyRole(CONTRACT_DESTROYER_ROLE) external {
        selfdestruct(payable(_msgSender()));
    }
}

Dex.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.14;
pragma experimental ABIEncoderV2;

import "./Wallet.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";


contract Dex is Wallet {
    using SafeMath for uint256;
    using Counters for Counters.Counter;
    using Math for uint;

    enum Side {
        BUY, 
        SELL
    }

    struct Order {
        uint id;
        address trader;
        Side side;
        bytes32 ticker;
        uint amount;
        uint price;
        uint filled;
    } 
    
    //ticker => SELL/BUY =>List of orders
    mapping(bytes32 => mapping (uint => Order[])) public orderBook;
    
    Counters.Counter private counter;
    
    constructor() Wallet(){
        Counters.reset(counter);
    }

    function getOrderBook (bytes32 ticker, Side side) view public returns(Order[] memory){
        return orderBook[ticker][uint(side)];
    }
    
    event LimitOrderCreated(bytes32 ticker, Side side, uint amount, uint price);
    
    function createLimitOrder(bytes32 _ticker, Side _side, uint _amount, uint _price ) tokenExist(_ticker) external returns (Order memory){
        if(_side == Side.BUY){
            require(balances[_msgSender()][bytes32("ETH")] >= SafeMath.mul(_amount, _price), "Sender doesn't have enougth ETH to buy"); 
        }
        else if(_side == Side.SELL){
            require(balances[_msgSender()][_ticker] >= _amount, "Sender doesn't have enougth TOKEN to sell");
        }
        Order[] storage orders = orderBook[_ticker][uint(_side)];
        
        Order memory order = Order(Counters.current(counter), _msgSender(), _side, _ticker, _amount, _price, 0);
        
        orders.push(order);
        emit LimitOrderCreated(_ticker, _side, _amount, _price);
        sortArray(orderBook[_ticker][uint(_side)], _side);
        Counters.increment(counter);
        return order;
    }

    event MarketOrderMatched(bytes32 ticker, Side side, uint amount, uint price);

    function createMarketOrder (bytes32 _ticker, Side _side, uint _value ) tokenExist(_ticker) external{
        if(_side == Side.SELL){
            require(balances[_msgSender()][_ticker] >= _value, "Sender doesn't have enougth TOKEN to sell");
        }

        uint other_side = SafeMath.mod(SafeMath.add(uint(_side), 1), 2);
        
        Order[] storage orders = orderBook[_ticker][other_side];
        uint marketOrderFilled = 0;
        bool continueMatchingOrders = true;
        uint index = 0;
        while ( continueMatchingOrders && orders.length > index){
            uint pendingAmount = SafeMath.sub(_value, marketOrderFilled);
            uint amountToFill = Math.min(pendingAmount, orders[index].amount);
            uint valueInEth =0;
            uint priceOrder = orders[index].price;
            uint buyerBalance = 0;
            if(_side == Side.BUY){
                buyerBalance = balances[_msgSender()][bytes32("ETH")];
                emit MarketOrderMatched(bytes32("ETH"), _side, buyerBalance, buyerBalance);
                require(buyerBalance >= priceOrder, "Sender doesn't have enougth ETH to buy 1 token");
                uint tokensCanBuy = SafeMath.div(buyerBalance, priceOrder);
                amountToFill  = Math.min(tokensCanBuy, amountToFill);               
            }
            orders[index].filled = SafeMath.add(orders[index].filled, amountToFill);
            valueInEth = SafeMath.mul(amountToFill, priceOrder);
            marketOrderFilled = SafeMath.add(marketOrderFilled, amountToFill);
            
            updateBalance(_msgSender(), _ticker, _side, valueInEth, amountToFill);
            updateBalance(orders[index].trader, _ticker, Side(other_side), valueInEth, amountToFill);
            
            emit MarketOrderMatched(_ticker, _side, amountToFill, priceOrder);
            continueMatchingOrders =  marketOrderFilled < _value && (buyerBalance > priceOrder && _side == Side.BUY || _side == Side.SELL);
            index = index +1;
        }
        
        uint lastCompleteFilledPosition = 0;
        bool shiftAndDeleteOrders = false;
        if (orders.length > 0){
            if (orders[index -1].amount == orders[index -1].filled){
            lastCompleteFilledPosition = index -1;
            shiftAndDeleteOrders = true;
            } else if (index > 1 && orders[index -1].amount != orders[index -1].filled){
                lastCompleteFilledPosition = index -2; //always positive as index > 1 ==> index >= 2 ==> index -2 >= 0
                shiftAndDeleteOrders = true;
            }     
            else{ 
                //index ==1 && orders[index -1].amount != orders[index -1].filled 
                //this case means that there are no orders to delete as no order was completed
                shiftAndDeleteOrders = false;
            }
        }
        
        if (shiftAndDeleteOrders==true){
            shiftOrderArray(orders, lastCompleteFilledPosition);
            deleteFilledOrders(orders, lastCompleteFilledPosition);
        }
                
    }

    function updateBalance(address account, bytes32 _ticker, Side _side, uint _value,  uint _amount) private {
        if(_side == Side.BUY){
            balances[account][bytes32("ETH")] = SafeMath.sub(balances[account][bytes32("ETH")], _value );  
            balances[account][_ticker] = SafeMath.add(balances[account][_ticker], _amount) ; 
        }
        else if(_side == Side.SELL){
            balances[account][_ticker] = SafeMath.sub(balances[account][_ticker],  _amount) ;
            balances[account][bytes32("ETH")] = SafeMath.add(balances[account][bytes32("ETH")], _value ); 
        }
    }

    function shiftOrderArray(Order[] storage orders, uint _lastCompleteFilledPosition) private {
        //while lastCompleteFilledPosition remains inside the array, shift the following elements
        uint indexCopy = 0;
        while (orders.length > _lastCompleteFilledPosition + indexCopy + 1){
            orders[indexCopy] = orders[_lastCompleteFilledPosition + indexCopy + 1];
            indexCopy = indexCopy +1;
        }
    }
    function deleteFilledOrders(Order[] storage orders, uint _lastCompleteFilledPosition) private {
        uint indexToDelete = 0;
        while (indexToDelete <= _lastCompleteFilledPosition ){
            orders.pop();            
            indexToDelete = indexToDelete +1;
        }
    }

    function sortArray(Order[] storage orders, Side _side) private {
        uint length = orders.length >0 ? orders.length -1 : 0;
        if (Side.BUY == _side ){
            for(uint i=length ; i>0 ; i--){    
                if (orders[i-1].price < orders[i].price){
                    Order memory aux = orders[i-1];
                    orders[i-1] = orders[i];
                    orders[i] = aux;
                }
            }
        }
        if (Side.SELL == _side ){
            for(uint i=length; i>0 ; i--){    
                if (orders[i-1].price > orders[i].price){
                    Order memory aux = orders[i-1];
                    orders[i-1] = orders[i];
                    orders[i] = aux;
                }
            }
        }
    }
}

dex_test.js

const Dex = artifacts.require("Dex")
const USDT = artifacts.require("USDT")
const truffleAssert = require('truffle-assertions')
const assert = require("chai").assert;
truffleAssert.ErrorType.REVERT='Revert'
contract("Dex", accounts=>{
    const USDT_TICKER = web3.utils.fromUtf8("USDT");
    const PRICE = 1; 
    const BUY = 0;
    const SELL = 1;
    
    it("Should throw an error if ticker dosn't exist", async () => {
        let dex = await Dex.deployed();
        await truffleAssert.reverts(
            dex.createLimitOrder(USDT_TICKER, SELL, 1, PRICE , {from: accounts[1]}), 
            "Token should be registered first"
        )
    }) 

    it("Should throw an error if ETH balance is too low when creating BUY limit order", async() => {
        let dex = await Dex.deployed();
        let usdt = await USDT.deployed();
        await dex.registerToken(USDT_TICKER, usdt.address, {from:accounts[0]});
        
        await truffleAssert.reverts(
           dex.createLimitOrder(USDT_TICKER, BUY, 1, PRICE , {from: accounts[1]}),
           "Sender doesn't have enougth ETH to buy"
        )
        
        await dex.depositEth({value:10,from: accounts[1]});
        await truffleAssert.passes(
            dex.createLimitOrder(USDT_TICKER, BUY, 1, PRICE , {from: accounts[1]})
        )

    })

    it("should throw an error if TOKEN balance is too low when creating SELL limit order", async () => {
        let dex = await Dex.deployed();
        let usdt = await USDT.deployed();
        
        await dex.registerToken(USDT_TICKER, usdt.address, {from:accounts[0]});
        
        await truffleAssert.reverts(
            dex.createLimitOrder(USDT_TICKER, SELL, 1, PRICE , {from: accounts[1]}),
            "Sender doesn't have enougth TOKEN to sell"
        )
        await usdt.transfer(accounts[1], 500);
        await usdt.approve(dex.address, 500, {from:accounts[1]});
        await dex.deposit(10, USDT_TICKER, {from:accounts[1]});
        await truffleAssert.passes(
            dex.createLimitOrder(USDT_TICKER, SELL, 1, PRICE , {from: accounts[1]})
        )
    })

    it("The Buy order book should be ordered on price from highest to lowest starting at index 0", async () => {
        let dex = await Dex.deployed();
        await dex.depositEth({value:100,from: accounts[1]});
        await dex.createLimitOrder(USDT_TICKER, BUY, 1, PRICE , {from: accounts[1]});
        await dex.createLimitOrder(USDT_TICKER, BUY, 1, PRICE+1 , {from: accounts[1]});
        await dex.createLimitOrder(USDT_TICKER, BUY, 1, PRICE+2 , {from: accounts[1]});
        let orders  = await dex.getOrderBook(USDT_TICKER, BUY);
        assert(orders.length >0, "there should be 3 orders");
        for(let i=0; i< orders.length-1; i++){
            await  assert(orders[i].price >= orders[i+1].price)
        }  

    })

    it("The Sell order book should be ordered on price from  lowest to highest starting at index 0", async () => {
        let dex = await Dex.deployed();
        let usdt = await USDT.deployed();
        await usdt.transfer(accounts[1], 500);
        await usdt.approve(dex.address, 500, {from:accounts[1]});
        await dex.deposit(10, USDT_TICKER, {from:accounts[1]});

        await dex.createLimitOrder(USDT_TICKER, SELL, 1, PRICE+2 , {from: accounts[1]});
        await dex.createLimitOrder(USDT_TICKER, SELL, 1, PRICE+1 , {from: accounts[1]});
        await dex.createLimitOrder(USDT_TICKER, SELL, 1, PRICE , {from: accounts[1]});
        let orders  = await dex.getOrderBook(USDT_TICKER, SELL);
        
        assert(orders.length >0, "there should be 3 orders");
        for(let i=0; i< orders.length-1; i++){
            await assert(orders[i].price <= orders[i+1].price)
        }  
    })

    it("createLimitOrder should emit LimitOrderCreated event", async () => {
        let dex = await Dex.deployed();
        let usdt = await USDT.deployed();
        await dex.registerToken(USDT_TICKER, usdt.address, {from:accounts[0]});
        await usdt.transfer(accounts[1], 500);
        await usdt.approve(dex.address, 500, {from:accounts[1]});
        await dex.deposit(10, USDT_TICKER, {from:accounts[1]});
        let createLimitOrder = await dex.createLimitOrder(USDT_TICKER, SELL, 1, PRICE+10 , {from: accounts[1]});
        
        truffleAssert.eventEmitted(createLimitOrder, "LimitOrderCreated", (ev) => {
            evTicker = web3.utils.hexToUtf8(ev.ticker) 
            ticker = web3.utils.hexToUtf8(USDT_TICKER)
            return evTicker == ticker && ev.side == SELL && ev.price.toNumber() == PRICE+10  && ev.amount.toNumber() ==1;
        }, 'TestEvent should be emitted with correct parameters');
    });
});

contract("Dex - Market orders", accounts=>{
    const USDT_TICKER = web3.utils.fromUtf8("USDT");
    const ETH_TICKER = web3.utils.fromUtf8("ETH");
    const LINK_TICKER = web3.utils.fromUtf8("LINK");
    const PRICE = 1; 
    const BUY = 0;
    const SELL = 1;
    describe("Market orders creation requirements tests", function(){
        it("Market order should throw an error if ticker dosn't exist", async () => {
            let dex = await Dex.deployed();
            await truffleAssert.reverts(
                dex.createMarketOrder(USDT_TICKER, SELL, 1, {from: accounts[1]}),
                "Token should be registered first"
            )
        });
        
        it("Market order should throw an error if TOKEN balance is too low when creating SELL limit order", async () => {
            let dex = await Dex.deployed();
            let usdt = await USDT.deployed();
            
            await dex.registerToken(USDT_TICKER, usdt.address, {from:accounts[0]});
            
            await truffleAssert.reverts(
                dex.createMarketOrder(USDT_TICKER, SELL, 1, {from: accounts[1]})
            )
            await usdt.transfer(accounts[1], 500);
            await usdt.approve(dex.address, 500, {from:accounts[1]});
            await dex.deposit(10, USDT_TICKER, {from:accounts[1]});
            await truffleAssert.passes(
                dex.createMarketOrder(USDT_TICKER, SELL, 1, {from: accounts[1]})
            )
        });

        it("Market order should throw an error if ETH balance is too low when creating BUY limit order", async() => {
            let dex = await Dex.deployed();

            await dex.createLimitOrder(USDT_TICKER, SELL, 10, PRICE+10 , {from: accounts[1]});

            await truffleAssert.reverts(
                dex.createMarketOrder(USDT_TICKER, BUY, 1, {from: accounts[1]}),
                "Sender doesn't have enougth ETH to buy"
            )

        });
    });
    describe("Market orders functionality tests", function(){
        let dex;
        let usdt;
        const fundingAccount = accounts[0];
        const buyerAccount = accounts[1];
        const sellerAccount = accounts[2];
        const anotherSellerAccount = accounts[3];
        const anotherBuyerAccount = accounts[4];

        let ETHBuyerAccountStartBalance;
        let USDTBuyerAccountStartBalance;
        let USDTAnotherBuyerAccountStartBalance;
        let ETHAnotherBuyerAccountStartBalance;
        let USDTSellerAccountStartBalance;
        let ETHSellerAccountStartBalance;
        let USDTAnotherSellerAccountStartBalance;
        let ETHAnotherSellerAccountStartBalance;

        let ordersBuyStart;
        let ordersSellStart;
        
        // build up and tear down a new Casino contract before each test
        beforeEach(async () => {
            dex = await Dex.new({ from: fundingAccount });
            usdt = await USDT.new({ from: fundingAccount });
            await dex.registerToken(USDT_TICKER, usdt.address, {from:fundingAccount});
            await usdt.transfer(sellerAccount, 500);
            await usdt.approve(dex.address, 500, {from:sellerAccount});
            await dex.deposit(100, USDT_TICKER, {from:sellerAccount});

            await usdt.transfer(anotherSellerAccount, 500);
            await usdt.approve(dex.address, 500, {from:anotherSellerAccount});
            await dex.deposit(100, USDT_TICKER, {from:anotherSellerAccount});

            await dex.depositEth({value:100,from: buyerAccount});
            await dex.depositEth({value:100,from: anotherBuyerAccount});
            
            
            ETHBuyerAccountStartBalance = (await dex.balances(buyerAccount, ETH_TICKER)).toNumber();
            ETHAnotherBuyerAccountStartBalance=(await dex.balances(anotherBuyerAccount, ETH_TICKER)).toNumber();
            ETHSellerAccountStartBalance= (await dex.balances(sellerAccount, ETH_TICKER)).toNumber();
            ETHAnotherSellerAccountStartBalance=(await dex.balances(anotherSellerAccount, ETH_TICKER)).toNumber();

            USDTBuyerAccountStartBalance= (await dex.balances(buyerAccount, USDT_TICKER)).toNumber();
            USDTAnotherBuyerAccountStartBalance= (await dex.balances(anotherBuyerAccount, USDT_TICKER)).toNumber();
            USDTSellerAccountStartBalance= (await dex.balances(sellerAccount, USDT_TICKER)).toNumber();
            USDTAnotherSellerAccountStartBalance= (await dex.balances(anotherSellerAccount, USDT_TICKER)).toNumber();
            
            assert.equal(USDTSellerAccountStartBalance, 100, "sellerAccount USDT start balance should be 100");
            assert.equal(USDTAnotherSellerAccountStartBalance, 100, "AnotherSellerAccount USDT start balance should be 100");
            assert.equal(USDTBuyerAccountStartBalance, 0, "buyerAccount USDT start balance should be 0");
            assert.equal(USDTAnotherBuyerAccountStartBalance, 0, "AnotherBuyerAccount USDT start balance should be 0");

            assert.equal(ETHSellerAccountStartBalance, 0, "sellerAccount ETH start balance should be 0");
            assert.equal(ETHAnotherSellerAccountStartBalance, 0, "AnotherSellerAccount ETH start balance should be 0");
            assert.equal(ETHBuyerAccountStartBalance, 100, "buyerAccount ETH start balance should be 100");
            assert.equal(ETHAnotherBuyerAccountStartBalance, 100, "AnotherBuyerAccount ETH start balance should be 100");
            
            ordersBuyStart  = await dex.getOrderBook(USDT_TICKER, BUY);
            assert.equal(ordersBuyStart.length, 0, "Buyer orderbook for ticker should be empty");
            ordersSellStart  = await dex.getOrderBook(USDT_TICKER, SELL);
            assert.equal(ordersSellStart.length, 0, "Seller orderbook for ticker should be empty");
        });

        afterEach(async () => {
            await dex.destroy({ from: fundingAccount });
        });

        //Market orders shouldn't do anything if the orderbook is empty 
        it("Market orders can be submited even if the orderbook is empty", async () => {
            
            let marketOrder = await dex.createMarketOrder(USDT_TICKER, SELL, 1, {from: sellerAccount})
            
            truffleAssert.eventNotEmitted(marketOrder, "MarketOrderMatched", (ev) => { 
                evTicker = web3.utils.hexToUtf8(ev.ticker) 
                ticker = web3.utils.hexToUtf8(USDT_TICKER)
                return evTicker == ticker && ev.side == SELL && ev.price.toNumber() * ev.amount.toNumber() > 1 ;
            }, 'MarketOrderMatched should not have been emitted');
            let orders_after  = await dex.getOrderBook(USDT_TICKER, BUY);
            assert.equal(orders_after.length, 0, "orderbook for ticker should remain empty");
            let USDTSellerAccountEndBalance = (await dex.balances(sellerAccount, USDT_TICKER)).toNumber();
            assert.equal(USDTSellerAccountStartBalance, USDTSellerAccountEndBalance);
        });

        it("Market orders created for exact amount of limit orders", async () => {
            //GIVEN
            await dex.createLimitOrder(USDT_TICKER, BUY, 1, PRICE , {from: anotherBuyerAccount});
            await dex.createLimitOrder(USDT_TICKER, BUY, 2, PRICE+10 , {from: buyerAccount});
            await dex.createLimitOrder(USDT_TICKER, BUY, 3, PRICE+20 , {from: buyerAccount});
            //Check valid assumptions
            ordersBuyStart  = await dex.getOrderBook(USDT_TICKER, BUY);
            assert.equal(ordersBuyStart.length, 3, "orderbook for buy ticker should not be empty");
            
            //TEST
            let marketOrder = await dex.createMarketOrder(USDT_TICKER, SELL, 3, {from: sellerAccount});

            //THEN
            truffleAssert.eventEmitted(marketOrder, "MarketOrderMatched", (ev) => {
                evTicker = web3.utils.hexToUtf8(ev.ticker) 
                ticker = web3.utils.hexToUtf8(USDT_TICKER)
                return evTicker == ticker && ev.side == SELL && ev.price.toNumber() == 21  && ev.amount.toNumber() ==3;
            }, 'MarketOrderMatched should have been emitted');

            ETHBuyerAccountEndBalance = (await dex.balances(buyerAccount, ETH_TICKER)).toNumber();
            ETHAnotherBuyerAccountEndBalance=(await dex.balances(anotherBuyerAccount, ETH_TICKER)).toNumber();
            ETHSellerAccountEndBalance= (await dex.balances(sellerAccount, ETH_TICKER)).toNumber();

            USDTBuyerAccountEndBalance= (await dex.balances(buyerAccount, USDT_TICKER)).toNumber();
            USDTAnotherBuyerAccountEndBalance= (await dex.balances(anotherBuyerAccount, USDT_TICKER)).toNumber();
            USDTSellerAccountEndBalance= (await dex.balances(sellerAccount, USDT_TICKER)).toNumber();
            let ordersBuyEnd  = await dex.getOrderBook(USDT_TICKER, BUY);
            assert.equal(ordersBuyEnd.length, ordersBuyStart.length -1, "orderbook for ticker should be one less");
            assert.equal(USDTSellerAccountEndBalance + 3 , USDTSellerAccountStartBalance, "Seller account should end with 3 USDT less");
            assert.equal(USDTBuyerAccountEndBalance , USDTBuyerAccountStartBalance +3, "Buyer account should end with 3 USDT more than started");
            assert.equal(ETHSellerAccountStartBalance + (PRICE+20) * 3, ETHSellerAccountEndBalance);
            assert.equal(ETHBuyerAccountStartBalance - (PRICE+20) * 3, ETHBuyerAccountEndBalance);

            assert.equal(ETHAnotherBuyerAccountStartBalance, ETHAnotherBuyerAccountEndBalance);
            assert.equal(USDTAnotherBuyerAccountStartBalance, USDTAnotherBuyerAccountEndBalance);
            
        });

       it("SELL Market orders should be filled until the order book is empty ", async () => {
            
            //GIVEN
            await dex.createLimitOrder(USDT_TICKER, BUY, 1, PRICE , {from: anotherBuyerAccount});
            await dex.createLimitOrder(USDT_TICKER, BUY, 2, PRICE+10 , {from: buyerAccount});
            await dex.createLimitOrder(USDT_TICKER, BUY, 3, PRICE+20 , {from: buyerAccount});
            //Check valid assumptions
            ordersBuyStart  = await dex.getOrderBook(USDT_TICKER, BUY);
            assert.equal(ordersBuyStart.length, 3, "orderbook for buy ticker should not be empty");
            
            //TEST
            let marketOrder = await dex.createMarketOrder(USDT_TICKER, SELL, 10, {from: sellerAccount});

            //THEN
            truffleAssert.eventEmitted(marketOrder, "MarketOrderMatched", (ev) => {
                let evTicker = web3.utils.hexToUtf8(ev.ticker); 
                let ticker = web3.utils.hexToUtf8(USDT_TICKER);
                let checkTickerAndSide = evTicker == ticker && ev.side == SELL;
                return checkTickerAndSide && (ev.price.toNumber() == 21  && ev.amount.toNumber() ==3) || 
                (ev.price.toNumber() == 11  && ev.amount.toNumber() ==2) || (ev.price.toNumber() == 1  && ev.amount.toNumber() ==1);
            }, 'MarketOrderMatched should have been emitted');

            ETHBuyerAccountEndBalance = (await dex.balances(buyerAccount, ETH_TICKER)).toNumber();
            ETHAnotherBuyerAccountEndBalance=(await dex.balances(anotherBuyerAccount, ETH_TICKER)).toNumber();
            ETHSellerAccountEndBalance= (await dex.balances(sellerAccount, ETH_TICKER)).toNumber();

            USDTBuyerAccountEndBalance= (await dex.balances(buyerAccount, USDT_TICKER)).toNumber();
            USDTAnotherBuyerAccountEndBalance= (await dex.balances(anotherBuyerAccount, USDT_TICKER)).toNumber();
            USDTSellerAccountEndBalance= (await dex.balances(sellerAccount, USDT_TICKER)).toNumber();
            ordersBuyEnd  = await dex.getOrderBook(USDT_TICKER, BUY);
            assert.equal(ordersBuyEnd.length, 0, "orderbook for ticker should be 0");
            assert.equal(USDTSellerAccountEndBalance + 6 , USDTSellerAccountStartBalance, "Seller account should end with 6 USDT less");
            assert.equal(USDTBuyerAccountEndBalance , USDTBuyerAccountStartBalance +5, "Buyer account should end with 5 USDT more than started");
            assert.equal(ETHSellerAccountStartBalance + (PRICE+20) * 3 + (PRICE + 10)* 2 + PRICE, ETHSellerAccountEndBalance);
            assert.equal(ETHBuyerAccountStartBalance - (PRICE+20) * 3 - (PRICE + 10)* 2, ETHBuyerAccountEndBalance);
            assert.equal(ETHAnotherBuyerAccountStartBalance - PRICE, ETHAnotherBuyerAccountEndBalance);
            assert.equal(USDTAnotherBuyerAccountStartBalance + 1, USDTAnotherBuyerAccountEndBalance);
        });
    

        it("BUY Market orders should be filled until the order book is empty ", async () => {
            
            //GIVEN
            await dex.createLimitOrder(USDT_TICKER, SELL, 1, PRICE , {from: anotherSellerAccount});
            await dex.createLimitOrder(USDT_TICKER, SELL, 2, PRICE+10 , {from: sellerAccount});
            await dex.createLimitOrder(USDT_TICKER, SELL, 3, PRICE+20 , {from: sellerAccount});
            //Check valid assumptions
            ordersSellStart  = await dex.getOrderBook(USDT_TICKER, SELL);
            assert.equal(ordersSellStart.length, 3, "orderbook for buy ticker should not be empty");
            
            //TEST
            let marketOrder = await dex.createMarketOrder(USDT_TICKER, BUY, 2, {from: buyerAccount});

            //THEN
            truffleAssert.eventEmitted(marketOrder, "MarketOrderMatched", (ev) => {
                let evTicker = web3.utils.hexToUtf8(ev.ticker); 
                let ticker = web3.utils.hexToUtf8(USDT_TICKER);
                let checkTickerAndSide = evTicker == ticker && ev.side == BUY;
                return checkTickerAndSide && (ev.price.toNumber() == 1  && ev.amount.toNumber() ==1) || 
                (ev.price.toNumber() == 11  && ev.amount.toNumber() ==1);
            }, 'MarketOrderMatched should have been emitted');

            ETHBuyerAccountEndBalance = (await dex.balances(buyerAccount, ETH_TICKER)).toNumber();
            ETHAnotherSellerAccountEndBalance=(await dex.balances(anotherSellerAccount, ETH_TICKER)).toNumber();
            ETHSellerAccountEndBalance= (await dex.balances(sellerAccount, ETH_TICKER)).toNumber();

            USDTBuyerAccountEndBalance= (await dex.balances(buyerAccount, USDT_TICKER)).toNumber();
            USDTAnotherSellerAccountEndBalance= (await dex.balances(anotherSellerAccount, USDT_TICKER)).toNumber();
            USDTSellerAccountEndBalance= (await dex.balances(sellerAccount, USDT_TICKER)).toNumber();
            
            let ordersSellEnd  = await dex.getOrderBook(USDT_TICKER, SELL);
            
            assert.equal(ordersSellEnd.length, 2, "orderbook for ticker should be 2");
            assert.equal(USDTSellerAccountEndBalance, USDTSellerAccountStartBalance - 1, "Seller account should end with 1 USDT less");
            assert.equal(USDTAnotherSellerAccountEndBalance, USDTAnotherSellerAccountStartBalance - 1, "Another Seller account should end with 1 USDT less");
            assert.equal(USDTBuyerAccountEndBalance , USDTBuyerAccountStartBalance + 2, "Buyer account should end with 2 USDT more than started");
            
            assert.equal(ETHSellerAccountStartBalance + (PRICE + 10), ETHSellerAccountEndBalance, "Seller account should end with 10 ETH more");
            assert.equal(ETHAnotherSellerAccountStartBalance + PRICE, ETHAnotherSellerAccountEndBalance, "Another Seller account should end with 1 ETH more");
            assert.equal(ETHBuyerAccountStartBalance - (PRICE + 10) - PRICE, ETHBuyerAccountEndBalance);
            
        });
    });
});

1 Like

Hello @filip @thecil
I’m completing the course and I’m having problems deploying to the testnet.
This is the error that I’m seeing, could you please help me with it?
Thanks in advance
Ale

const newErr = new Error(`PollingBlockTracker - encountered an error while attempting to update latest block:\n${err.stack}`)
                       ^
Error: PollingBlockTracker - encountered an error while attempting to update latest block:
undefined
    at PollingBlockTracker._performSync (/Users/alejandromildiner/Documents/personal/first_dex_dapp/node_modules/eth-block-tracker/src/polling.js:51:24)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
1 Like

Hey Mildo, hope you are great!

You might wanna take a look at this issue, apparently it have a solution proposed from some users
https://github.com/trufflesuite/truffle/issues/3357

Carlos Z

1 Like

Hello Carlos, thanks for the quick response I’ll look into it

1 Like

Hello Carlos, I’ve checked that post and by changing the https for wss
from https://togxoqqiuzsi.usemoralis.com:2053/server
to wss://togxoqqiuzsi.usemoralis.com:2053/server
I now get a timeout error.
Can I send you screenshots of the moralis server config? I think the issue is related with authentication as the error also shows this (not every time)

UnhandledRejections detected
Promise {
  <rejected> {
    code: -32603,
    message: 'Unknown Error: {"error":"unauthorized"}',
    data: { originalError: {} }
  }
} {
  code: -32603,
  message: 'Unknown Error: {"error":"unauthorized"}',
  data: { originalError: {} }
}
Promise {
  <rejected> {
    code: -32603,
    message: 'Unknown Error: {"error":"unauthorized"}',
    data: { originalError: {} }
  }
} {
  code: -32603,
  message: 'Unknown Error: {"error":"unauthorized"}',
  data: { originalError: {} }
}
Promise {
  <rejected> {
    code: -32603,
    message: 'Unknown Error: {"error":"unauthorized"}',
    data: { originalError: {} }
  }
} {
  code: -32603,
  message: 'Unknown Error: {"error":"unauthorized"}',
  data: { originalError: {} }
}

Regards
Ale

1 Like

hey Mldo you can send them on to me. and i will check what is up

If you are using our nodes to connect with the blockchain, sadly that might not be possible for now, we have deprecated our speedy nodes.

You can read more here: https://moralis.zendesk.com/hc/en-us/articles/6086685831570-Speedy-Nodes-Removal-for-FREE-plan-users-Starting-July-11th-2022-

I advice you to create an account and use their nodes at https://www.alchemy.com/

Carlos Z

sure thanks @mcgrane5

const HDWalletProvider = require('@truffle/hdwallet-provider');
const mnemonic = require("./secret.json").secret;

module.exports = {
  
  networks: {
    ropsten: {
      provider: () => new HDWalletProvider(mnemonic, `https://togxoqqiuzsi.usemoralis.com:2053/server`),
      network_id: 3,       // Ropsten's id
      gas: 5500000,        // Ropsten has a lower block limit than mainnet
      confirmations: 2,    // # of confirmations to wait between deployments. (default: 0)
      timeoutBlocks: 200,  // # of blocks before a deployment times out  (minimum/default: 50)
      skipDryRun: true     // Skip dry run before migrations? (default: false for public nets )
    }
  },

  // Configure your compilers
  compilers: {
    solc: {
      version: "0.8.14",      // Fetch exact version from solc-bin (default: truffle's version)
      // docker: true,        // Use "0.5.1" you've installed locally with docker (default: false)
      // settings: {          // See the solidity docs for advice about optimization and evmVersion
      //  optimizer: {
      //    enabled: false,
      //    runs: 200
      //  },
      //  evmVersion: "byzantium"
      // }
    }
  },

};

These pics show the dapp configuration in the moralis dapps page


Regards
Mildo

1 Like

so the moralis dapps server are not working?
There is a banner message stating that for production we need to contact you for having the correct plan…
I just want to deploy to any test network to test my dapp, should I use alchemy?
Regards
Mildo

1 Like