Hi,
Here is my code, I expect to build a front end for it in a month of so, I feel that I will do first the Ethereum dApp programming course, it would be easier and good practice at the end of that one.
Besides this code mostly follows Philip’s code I made all the challenges myself and ended sometimes desperate, also it helped each time more, and by the end I could came up with solutions for my own, few times even they worked
dex.sol
pragma solidity ^0.8.0;
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[])) public orderBook;
function getOrderBook (bytes32 ticker, Side side) view public returns (Order [] memory){
return orderBook[ticker][uint(side)];
}
function createLimitOrder(Side side, bytes32 ticker, uint256 amount, uint256 price ) public {
if(side == Side.BUY){
require(balances[msg.sender]["ETH"] >= amount.mul(price));
}
if(side== Side.SELL){
require(balances[msg.sender][ticker] >= amount);
}
Order[] storage orders = orderBook[ticker][uint(side)]; //this is a reference to storage, not storage
orders.push(Order( nextOrderId, msg.sender, side, ticker, amount, price,0) ); //but it goes directly to storage
//bubble sort
uint i = orders.length>0 ? orders.length-1: 0;
if( side == Side.BUY){ //the highest price first
while( i > 0 ) {
if ( orders[i].price < orders[i-1].price){
break;
}
Order memory temp =orders[i];
orders[i] = orders[i-1];
orders[i-1] = temp;
i--;
}
}
else if(side== Side.SELL){
while(i>0) {
if ( orders[i].price > orders[i-1].price){
break;
}
Order memory temp =orders[i];
orders[i] = orders[i-1];
orders[i-1] = temp;
i--;
}
}
nextOrderId++;
}
function createMarketOrder(Side side, bytes32 ticker, uint amount) public {
if(side== Side.SELL){
require(balances[msg.sender][ticker] >= amount);
}
uint orderBookSide;
if(side == Side.BUY){
orderBookSide = 1;
}
else {
orderBookSide=0;
}
Order[] storage orders = orderBook[ticker][uint(orderBookSide)];
uint totalFilled = 0;
for (uint i= 0; i< orders.length && totalFilled< amount; i++){
uint leftToFilled = amount.sub(totalFilled);
//How much we can fill from order[i]
uint available= orders[i].amount.sub(orders[i].filled);
uint filled =0; //Use instead of orders[i].filled because the transaction may fail
if ( available <= leftToFilled ){
filled = available;
}else if ( available > leftToFilled) {
filled = leftToFilled;
}
//Update totalFilled
totalFilled= totalFilled.add(filled);
//Here we have to difference between Buy side and sell Side
if(side == Side.BUY){
//verify that the buyer trader has enough ETH to cover the purchase (require)
//execute the trade & shift balances betweent buyer/seller
require(balances[msg.sender]["ETH"]>=filled.mul(orders[i].price), "something happens here");
balances[orders[i].trader][ticker] = balances[orders[i].trader][ticker].sub(filled);
balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].sub(filled * orders[i].price);
balances[orders[i].trader]["ETH"]=balances[orders[i].trader]["ETH"].add(orders[i].price * filled);
balances[msg.sender][ticker] = balances[msg.sender][ticker].add(filled);
}
else if (side == Side.SELL){
//execute the trade & shift balances betweent buyer/seller
balances[orders[i].trader]["ETH"] = balances[orders[i].trader]["ETH"].sub(filled.mul(orders[i].price));
balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(filled);
balances[orders[i].trader][ticker]=balances[orders[i].trader][ticker].add(filled);
balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"].add(orders[i].filled);
}
orders[i].filled = filled;
}
while( orders.length >0 && orders[0].filled ==orders[0].amount ){
for (uint i= 0; i< orders.length-1; i++){
orders[i] = orders[i+1];
}
orders.pop();
}
}
}
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; //we have to know where we need to do transfers calls to make the token contract (we will need the interface of the token)
}
mapping (bytes32 => Token) public tokenMapping; //information about the tokens
bytes32[] public tokenList; //list to have ll of teh tickers ids
mapping(address => mapping( bytes32 => uint256)) public balances; //we do not use string, cannot compare in solidity balances of the investors
modifier tokenExist(bytes32 ticker){
require(tokenMapping[ticker].tokenAddress != address(0), "token does not exists"); //requiere to token to exits ( if not it would be address 0)
_;
}
function addToken(bytes32 ticker, address tokenAddress) external onlyOwner(){
tokenMapping[ticker] = Token(ticker, tokenAddress);
tokenList.push(ticker);
}
function deposit( uint amount, bytes32 ticker) external tokenExist(ticker){
require(amount > 0, "You need to deposit at least some tokens");
require(IERC20(tokenMapping[ticker].tokenAddress).balanceOf(msg.sender)>= amount," insufficient funds");
IERC20(tokenMapping[ticker].tokenAddress).transferFrom(msg.sender,address(this), amount);//the allowance has to be given before, we could check but it is checked already here
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 withdraw(uint amount, bytes32 ticker) external tokenExist(ticker) {
require(amount > 0, "You need to withdraw at least some tokens");
require(balances[msg.sender][ticker]>= amount, " insufficient balance");
balances[msg.sender][ticker] = balances[msg.sender][ticker].sub(amount);
IERC20(tokenMapping[ticker].tokenAddress).transfer(msg.sender, amount); //we need to interact with the ERC 20 token contract now, we need the address of (IERC20) and the interface
}
}
dextest.js
const Dex = artifacts.require("Dex");
const Link = artifacts.require("Link");
const truffleAssert =require("truffle-assertions");
contract("Dex", accounts => {
xit("The user have Eth deposited such that, deposited eth >= buy order value", async () =>
{
let dex = await Dex.deployed()
let link = await Link.deployed()
await dex.addToken(web3.utils.fromUtf8("LINK"), link.address, {from: accounts[0]})
await dex.addToken(web3.utils.fromUtf8("ETH"), link.address, {from: accounts[0]})
await truffleAssert.reverts(
dex.createLimitOrder(0, web3.utils.fromUtf8("LINK"), 10, 1)
)
await dex.depositEth({value: web3.utils.toWei("10", "ether")})
//await console.log(dex.balances(accounts[0], web3.utils.fromUtf8("ETH")))
await truffleAssert.passes(
dex.createLimitOrder(0, web3.utils.fromUtf8("LINK"), 10, 1)
)
})
xit("The user must have enough tokens deposited such that token balance >= sell order amount", async () =>{
let dex = await Dex.deployed()
let link = await Link.deployed()
// await dex.addToken(web3.utils.fromUtf8("LINK"), link.address, {from: accounts[0]})
await truffleAssert.reverts(
dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 10, 1)
)
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)
)
})
xit("The BUY order Book should be ordered on price from highest to lowest starting at index 0", async() =>{
let dex = await Dex.deployed()
let link = await Link.deployed()
//await dex.addToken(web3.utils.fromUtf8("LINK"), link.address, {from: accounts[0]})
//await link.approve(dex.address, 500);
await dex.createLimitOrder(0, web3.utils.fromUtf8("LINK"), 1, 300)
await dex.createLimitOrder(0, web3.utils.fromUtf8("LINK"), 1, 100)
await dex.createLimitOrder(0, web3.utils.fromUtf8("LINK"), 1, 200)
let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 0);
assert (orderbook.length >0)
for (let i = 0; i < orderbook.length - 1; i++) {
//const element = array[index];
assert(orderbook[i].price >= orderbook[i+1].price)
}
})
xit("The SELL order Book should be ordered on price from lowest to highest starting at index 0 ", async() =>{
let dex = await Dex.deployed()
let link = await Link.deployed()
//await dex.addToken(web3.utils.fromUtf8("LINK"), link.address, {from: accounts[0]})
//await link.approve(dex.address, 1000);
//await dex.deposit(800,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)
let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1);
//console.log(orderbook)
//assert(orderbook.length==4)
assert (orderbook.length >0)
for (let i = 0; i < orderbook.length - 1; i++) {
//const element = array[index];
assert(orderbook[0].price <= orderbook[i+1].price)
}
})
})
//Tests for Market Order
contract ("Dex", accounts => {
//when creating a SELL market order, the seller needs to have enough tokens for the trade
it("when creating a SELL market order, the seller needs to have enough tokens for the trade", async () => {
let dex = await Dex.deployed()
let link = await Link.deployed()
await dex.addToken(web3.utils.fromUtf8("LINK"), link.address, {from: accounts[0]})
await truffleAssert.reverts(
dex.createMarketOrder(1, web3.utils.fromUtf8("LINK"), 10 )
)
await link.approve(dex.address, 500);
await dex.deposit(10, web3.utils.fromUtf8("LINK"));
await truffleAssert.passes(
dex.createMarketOrder(1, web3.utils.fromUtf8("LINK"), 10)
)
})
//when creating a BUy market order, the buyer needs to have enough ETH for the trade
it (" when creating a BUY market order, the buyer needs to have enough ETH for the trade ", async () =>{
let dex = await Dex.deployed()
let link = await Link.deployed()
await dex.addToken(web3.utils.fromUtf8("ETH"), link.address, {from: accounts[0]})
await dex.depositEth({value: web3.utils.toWei("10", "ether")})
//await console.log(dex.balances(accounts[0], web3.utils.fromUtf8("ETH")))
await truffleAssert.passes(
dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 10)
)
})
it ( "Market orders should not fill more limit orders than the market orders amount", async ()=>{
let dex = await Dex.deployed()
let link = await Link.deployed()
await dex.depositEth({value: 10000});
let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1);
assert(orderbook.length ==0, "buy side Orderbook is not empty")
//send link to the accounts 1 ,2,3 froom account 0
await link.transfer(accounts[1],50)
await link.transfer(accounts[2],50)
await link.transfer(accounts[3],50)
let balance = await link.balanceOf(accounts[1]);
console.log(balance.toNumber());
//aprove DEX for Accounts 1,2,3
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]});
//deposit Link into DEX for accounts, 1,2,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]});
//fill up the orderBook
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]})
//Create a market order that should fill 2/ orders in the book
await dex.createMarketOrder(0,web3.utils.fromUtf8("LINK"), 10)
orderbook= await dex.getOrderBook(web3.utils.fromUtf8("LINK"),1);
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");
})
//Market orders should be filled until the order book is empty
it ("Market orders should be filled until the order book is empty", async ()=>{
let dex = await Dex.deployed();
let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1);
await console.log(orderbook.length)
assert(orderbook.length ==1, "Sell side Orderbook should have 1 order left ");
//fill up the orderbook again
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]})
let balanceBefore = await dex.balances(accounts[0], web3.utils.fromUtf8("LINK"))
await dex.createMarketOrder(0,web3.utils.fromUtf8("LINK"), 150);
//check the buyer balance after
let balanceAfter = await dex.balances(accounts[0], web3.utils.fromUtf8("LINK", 50 ));
assert (balanceBefore+15, balanceAfter);
})
//the ETH balance of the buyer should decrease with the filled amount
it ("the ETH balance of the buyer should decrease with the filled amount ", async ()=>{
let dex = await Dex.deployed()
let link = await Link.deployed()
//set some order depositing link in account 1
await link.approve(dex.address, 500, {from: accounts[1]});
await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 1, 300, {from: accounts[1]})
//check the buyer balance before trade
let balanceBefore = await dex.balances(accounts[0], web3.utils.fromUtf8("ETH"));
await dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"),1);
let balanceAfter = await dex.balances(accounts[0], web3.utils.fromUtf8("ETH"));
assert.equal(balanceBefore-300, balanceAfter);
})
//the token balances of the sellers should decrease wiht the filled amount.
it("the token balances of the sellers should decrease with the filled amount.", async ()=>{
let dex = await Dex.deployed()
let link = await Link.deployed()
let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1);
//assert(orderbook.length ==0, "buy side Orderbook is not empty")
await link.transfer(accounts[2],500)
//he says accoutn1 already have links.
//seler 2 desposits link
//await link.approve(dex.address, 500, {from: accounts[1]});
await link.approve(dex.address, 500, {from: accounts[2]});
//await dex.deposit(100, web3.utils.fromUtf8("LINK"), {from : accounts[1]})
await dex.deposit(100, web3.utils.fromUtf8("LINK"), {from : accounts[2]})
//set some buy limit orders
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]})
let account1balanceBefore = await dex.balances(accounts[1], web3.utils.fromUtf8("LINK")); //get the balances previous the sale
let account2balanceBefore = await dex.balances(accounts[2], web3.utils.fromUtf8("LINK"));
await dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 2)
let account1balanceAfter = await dex.balances(accounts[1], web3.utils.fromUtf8("LINK")); //balance after the sale
let account2balanceAfter = await dex.balances(accounts[2], web3.utils.fromUtf8("LINK"));
assert.equal(account1balanceBefore -1, account1balanceAfter);
assert.equal(account2balanceBefore -1, account2balanceAfter);
})
//filled limit orders should be removed from the orderbook
it("filled limit orders should be removed from the orderbook", async ()=>{
let dex = await Dex.deployed()
let link = await Link.deployed()
let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1);
assert(orderbook.length ==0, "sell side Orderbook is not empty")
await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 1, 300, {from: accounts[1]})
await dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 1)
orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1);
assert(orderbook.length == 0, "Seel side Orderbook should be empty after trade");
})
//partly filled limit orders should modified to represent the filled/remaining amount
it("limit orders filled property should be set correctly after a trade ", async ()=>{
let dex = await Dex.deployed()
let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1);
assert(orderbook.length ==0, "sell side Orderbook is not empty")
await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 5, 300, {from: accounts[1]})
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);
})
})
walletTest.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 () =>
{
// await deployer.deploy(Link);
let dex = await Dex.deployed();
let link = await Link.deployed();
await truffleAssert.passes(
dex.addToken(web3.utils.fromUtf8("LINK"), link.address, {from: accounts[0]})
)
await truffleAssert.reverts(
dex.addToken(web3.utils.fromUtf8("Ljhkhlk"), link.address, {from: accounts[1]})
)
})
it("should handle deposit correctly", async ()=>{
let dex = await Dex.deployed();
let link = await Link.deployed();
await dex.addToken(web3.utils.fromUtf8("LINK"), link.address, {from: accounts[0]})
await link.approve( dex.address, 500);
await dex.deposit(100, web3.utils.fromUtf8("LINK"));
let balance = await dex.balances(accounts[0], web3.utils.fromUtf8("LINK"));
assert.equal(balance,100);
} )
it("should handle withdraws correctly", async () =>{
let dex = await Dex.deployed();
let link = await Link.deployed();
await link.approve( dex.address, 500);
await dex.withdraw(50, web3.utils.fromUtf8("LINK") )
let balance = await dex.balances(accounts[0], web3.utils.fromUtf8("LINK"));
assert.equal(balance.toNumber(),50);
await truffleAssert.reverts(
dex.withdraw(50, web3.utils.fromUtf8("LINK"), {from:accounts[1]} )
)
})
})