hey @William. Sorry for getting back so late but I had a look at your code there and there was a few things. So firstly the main reason you were getting bazzar outputs akin to what you described above was because of a small typo that was messing up the sell side of your order book (which actuallty took me awhile to find). The bug or rather typo, lied withtin the createLimitOrder( ) function. The problem is that when you created an instance of the orderbook i.e assinging it to some var you made a mistake when your inputted your orderbook āsideā. You originally had written
TransactionOrder[] storage transactionOrders = orderbook[ticker][uint(side)];
should have been
TransactionOrder[] storage transactionOrders = orderbook[ticker][uint(_side)];
So all you needed was to remain consistent with your var names. This is the reason for all of your unexpected order book errors. Previously the way you had it the sell orders never got appended and this is what caused your tests to fail. This little bug is the reason why the orderboook on the sell side would not update. Instead what was happening was all of the orders were going into the same orderbook well thats what it looked like to me from terminal output. Regardless that is the fix
Now onto the tests. Tour tests needed some minor adjustments too. Firstly you were approving the ālinkā token for use, but you never added it. You need to add the token before you can approve it. Also when i used your original limit orders, they failed because the summation of the price multiplied by th eorginal amounts you had used, went above the amount of tokens that you had approved for the link token. So i just changed you amounts and prices to be below the aprrove allowance. I also just deposited link in the sell order test. I know you mint 1000 on the contract creation but i just deposited link tokens in this test anyways. Other than that there was very minimal changes i made to your code but the files are below, the main thing was just the typo in your smart contract,
TransactionOrder[] storage transactionOrders = orderbook[ticker][uint(_side)];
TEST FILE
const Dex = artifacts.require("Dex")
const Token = artifacts.require("Token")
const truffleAssert = require('truffle-assertions');
contract("Dex", accounts => {
it("BUY order book should be ordered on price from highest to lowest starting at index 0", async () => {
let dex = await Dex.deployed()
let link = await Token.deployed()
//add token to the exchane first
dex.addToken( web3.utils.fromUtf8("LINK"), link.address);
await link.approve(dex.address, 500);
await dex.deposit(200, web3.utils.fromUtf8("LINK"))
await dex.depositETH({value: 3000});
await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 1, 1, 300)
await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 1, 1, 100)
await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 1, 1, 200)
let buyOrderBook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1);
assert(buyOrderBook.length > 0);
// console.log(buyOrderBook);
for (let i = 0; i < buyOrderBook.length - 1; i++) {
assert(buyOrderBook[i].price <= buyOrderBook[i+1].price)
}
})
it("SELL OrderBook is in order from Lowest to Highest, starting at index[0]", async() => {
let dex = await Dex.deployed()
let link = await Token.deployed()
await dex.addToken(web3.utils.fromUtf8("LINK"), link.address)
await link.approve(dex.address, 1000);
await dex.depositETH({from: accounts[0], value: 1000});
await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 0, 1, 200)
await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 0, 1, 100)
await dex.createLimitOrder(web3.utils.fromUtf8("LINK"), 0, 2, 400)
let sellOrderBook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 0);
// assert(buyOrderBook.length > 0);
console.log(sellOrderBook);
for (let i = 0; i < sellOrderBook.length - 1; i++){
// console.log(sellOrderBook[i]);
assert(sellOrderBook[i].price >= sellOrderBook[i + 1].price, "buy-order book is out of order.")
}
// console.log(sellOrderBook.length);
})
})
DEX
pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;
import "./Wallet.sol";
contract Dex is Wallet {
using SafeMath for uint256;
enum SIDE {BUY, SELL}
SIDE side;
//Building the transaction information
struct TransactionOrder {
uint id;
address trader;
SIDE side;
bytes32 ticker;
uint amount;
uint price;
}
uint public nextOrderId = 0;
//need an ORDERBOOK split into two parts -->
//bids & asks
//AND an orderbook for each asset
mapping (address => uint256) public EthBalance;
//order points to a ticker which points to a Enum option (uint 0, 1, etc)
mapping (bytes32 => mapping (uint => TransactionOrder[])) public orderbook;
//depositing ETH requires the dex.depositETH({value: X}); command
function depositETH() public payable returns(bool) {
require(msg.value > 0, "Insufficient value.");
EthBalance[msg.sender]= EthBalance[msg.sender].add(msg.value);
return true;
}
//get the orderbook --> need the bytes32 ticker and the SIDE (BUY OR SELL)
//view because it just returns something
//and to input this function --> an example = getOrderBook(bytes32("LINK"), SIDE.BUY);
//Solidity automatically converts SIDE.BUY into an integer that reads "0" or "1"
//with order they are presented in enum
function getOrderBook (bytes32 ticker, SIDE _side) view public returns(TransactionOrder[] memory) {
return orderbook[ticker][uint(_side)];
}
//Complex function to create... why?
//as soon as you add an order into the order book, you need to sort it --> needs to be in proper position
//best Buy price is at HIGHEST side of BIDS orderbook
//best Sell price is at LOWEST side of ASKS orderbook
//loops are needed for this
//args (ticker, uint 0/1 = buy/sell, uint how many tokens do you want to buy/sell, uint price of token)
function createLimitOrder(bytes32 ticker, SIDE _side, uint amount, uint price) public tokenExists(ticker) {
if(_side == SIDE.BUY){
require(EthBalance[msg.sender] >= amount.mul(price));
}
else if(_side == SIDE.SELL){
require(balances[msg.sender][ticker] >= amount);
}
TransactionOrder[] storage transactionOrders = orderbook[ticker][uint(_side)];
//This TxOrder (below) will be pushed into the TxOrder[] above^, we've already extracted if it's Buy/Sell
transactionOrders.push(TransactionOrder(nextOrderId, msg.sender, _side, ticker, amount, price));
//bubble sort
//[6, 5, 4, 3]
//setting the last element in the array to i using an if statement as a variable
uint i = transactionOrders.length > 0 ? transactionOrders.length - 1 : 0;
if (_side == SIDE.BUY){
while(i > 0){
//if the last element.price minus 1 > than the last element (i) in the array, then...
//array is sorted properly
if(transactionOrders[i - 1].price > transactionOrders[i].price){
break;
}
TransactionOrder memory orderToMove = transactionOrders[i - 1];
transactionOrders[i - 1] = transactionOrders[i];
transactionOrders[i] = orderToMove;
//decrease value of i and do it again...
i--;
}
}
//[2, 3, 3]
//sellOrderToMove = 5
if (_side == SIDE.SELL){
while(i > 0){
if(transactionOrders[i - 1].price < transactionOrders[i].price){
break;
}
TransactionOrder memory sellOrderToMove = transactionOrders[i - 1];
transactionOrders[i - 1] = transactionOrders[i];
transactionOrders[i] = sellOrderToMove;
i--;
}
}
}
}
TOKEN
pragma solidity ^0.8.0;
import "../node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract Token is ERC20 {
constructor() ERC20("Chainlink", "LINK") {
_mint(msg.sender, 1000);
}
}
Wallet
pragma solidity ^0.8.0;
import "../node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../node_modules/@openzeppelin/contracts/utils/math/SafeMath.sol";
import "../node_modules/@openzeppelin/contracts/access/Ownable.sol";
//This should be Ownable
contract Wallet is Ownable {
using SafeMath for uint256;
event Deposit(address sender, uint amount, uint balance);
//Since we are interacting with other ERC20 token contracts,
//we need to have a way to store information about these different tokens
//WHY "tokenAddress?" --> whenever you create something
//you need to be able to do transfer calls WITH that created ERC20 contract
struct Token {
bytes32 ticker;
address tokenAddress;
}
//In order for the DEX to be able to trade the token later on,
//it needs support for that token (needs the tokenAddress saved somewhere) --
//SAVE this address in a combined structure between an array and a mapping
//can get the tokens and update them quickly here
mapping (bytes32 => Token) public tokenMapping;
//Saves all of the tickers (unique)
//can loop through all of the tokens, just can't delete
bytes32[] public tokenList;
//create a double mapping for the balances (every user/trader will have a balance of different tokens)
//will be able to deposit both ETH and ERC20 tokens (can have ETH, LINK, AAVE, etc...)
//need a mapping that supports multiple balances
//mapping is an address that points to another mapping that holds the tokenID (expressed with bytes32) and amount
//Why bytes32? B/C in Solidity, you can't compare strings (can't string = string)
//Instead, you can convert the "token symbol" into bytes32
mapping (address => mapping(bytes32 => uint256)) public balances;
//instead of adding a bunch of require() statements that chekc for the same thing
//we will create a modifier and just add it to the function header, remove the require code from body
modifier tokenExists(bytes32 ticker) {
require(tokenMapping[ticker].tokenAddress != address(0));
_;
}
//Create an ADD token FUNCTION so we can add to our DEX
//bytes32 ticker --> give it it's ticker symbol and the bytes32 makes it comparable (can't do string = string)
//address tokenAddress --> to access this token's "home" contract to interact with it
//why external? Won't need to execute this from inside this contract
function addToken(bytes32 ticker, address tokenAddress) onlyOwner external {
tokenMapping[ticker] = Token(ticker, tokenAddress);
tokenList.push(ticker);
}
//Pull cryptoassets in FROM another contract address
//increase depositer balances[address][ticker] = balances[address][ticker].add(amount)
function deposit(uint amount, bytes32 ticker) tokenExists(ticker) external {
IERC20(tokenMapping[ticker].tokenAddress).transferFrom(msg.sender, address(this), amount);
balances[msg.sender][ticker] = balances[msg.sender][ticker].add(amount);
//emit Deposit(msg.sender, msg.value, address(this).balance);
}
//Why do you check if the tokenAddress is not the 0 address?
//If this mapping of the specific ticker points to an unitialized struct, all of the data will be 0
function withdraw(uint amount, bytes32 ticker) tokenExists(ticker) external {
require(balances[msg.sender][ticker] >= amount, "Balance not sufficient.");
balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(amount);
//IERC20(address of the token in THIS contract).transfer(recipient address of where it's going, amount)
IERC20(tokenMapping[ticker].tokenAddress).transfer(msg.sender, amount);
}
}
**EDIT i actually sorry i chaged a few of your dex and token contract var names only just because the project folder that i use to debug other peoples code sort of has a format and i just changed your contract var names. Just mentioning thus incase you were wondering why.