Wallet
pragma solidity ^0.8.0;
import "../node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../node_modules/@openzeppelin/contracts/access/Ownable.sol";
contract Wallet is Ownable {
struct Token {
bytes32 ticker;
address tokenAddress;
}
mapping(bytes32 => Token) public tokenMapping;
bytes32[] public tokenList;
//Mapping address to tokenID to balance
mapping(address =>mapping(bytes32 => uint)) public balances;
modifier tokenExist(bytes32 ticker_){
require(tokenMapping[ticker_].tokenAddress != address(0), "exchange doesn't support token");
_;
}
function addToken(bytes32 ticker_, address tokenAddress_) external onlyOwner {
if(tokenMapping[ticker_].tokenAddress == address(0)){
tokenMapping[ticker_] = Token(ticker_,tokenAddress_);
tokenList.push(ticker_);
}
else {
tokenMapping[ticker_] = Token(ticker_,tokenAddress_);
}
}
function deposit(uint amount_, bytes32 ticker_) external tokenExist(ticker_) {
IERC20(tokenMapping[ticker_].tokenAddress).transferFrom(msg.sender, address(this), amount_);
balances[msg.sender][ticker_] += amount_;
}
function withdraw(uint amount_, bytes32 ticker_) external tokenExist(ticker_) {
require(balances[msg.sender][ticker_] >= amount_, "not enough balance");
balances[msg.sender][ticker_] -= amount_;
IERC20(tokenMapping[ticker_].tokenAddress).transfer(msg.sender, amount_);
}
function depositEth() external payable{
balances[msg.sender]["ETH"] += msg.value;
}
function withdrawEth(uint amount_) external {
require(balances[msg.sender]["ETH"] >= amount_, "You are trying to withdraw more than you have!");
balances[msg.sender]["ETH"] -= amount_;
payable(msg.sender).transfer(amount_);
}
}
Dex
pragma solidity ^0.8.0;
import "./wallet.sol";
contract Dex is Wallet{
enum Side { BUY, SELL }
struct Order {
uint id;
address trader;
bytes32 ticker_;
Side side_;
uint amount_;
uint price;
uint filled;
}
uint public nextOrderId = 0;
mapping (bytes32=>mapping(uint =>Order[])) orders;
function getOrderBook(bytes32 ticker_, Side side_) public view returns (Order[] memory){
return orders[ticker_][uint(side_)];
}
function createLimitOrder(Side side_, bytes32 ticker_, uint amount__, uint price_) public {
if(side_ == Side.BUY){
require(balances[msg.sender]["ETH"] >= amount__ * price_, "BUY ORDER: not enought eth to place order!");
}
else if(side_ == Side.SELL){
require(balances[msg.sender][ticker_] >= amount__, "SELL ORDER: not enought tokens to place order!");
}
Order[] storage orders_ = orders[ticker_][uint(side_)];
orders_.push(Order(nextOrderId,msg.sender,ticker_, side_, amount__, price_, 0));
Order memory _order;
//Bubble sort
if(side_ == Side.BUY){
for(uint i = orders_.length -1; i > 0; --i){
uint nextIndex = i-1;
if(orders_[i].price > orders_[nextIndex].price){
_order = orders_[i];
orders_[i] = orders_[nextIndex];
orders_[nextIndex] = _order;
}
}
}
else if(side_ == Side.SELL){
for(uint i = orders_.length -1; i > 0; --i){
uint nextIndex = i-1;
if(orders_[i].price < orders_[nextIndex].price){
_order = orders_[i];
orders_[i] = orders_[nextIndex];
orders_[nextIndex] = _order;
}
}
}
++nextOrderId;
}
function createMarketOrder(Side side_, bytes32 ticker_, uint amount_) public {
if(side_ == Side.SELL){
require(balances[msg.sender][ticker_] >= amount_, "Insuffient balance");
}
uint orderBookSide;
if(side_ == Side.BUY){
orderBookSide = 1;
}
else{
orderBookSide = 0;
}
Order[] storage _orders = orders[ticker_][orderBookSide];
uint totalFilled = 0;
for (uint256 i = 0; i < _orders.length && totalFilled < amount_; i++) {
uint leftToFill = amount_ - totalFilled;
uint availableToFill = _orders[i].amount_ - _orders[i].filled;
uint filled = 0;
if(availableToFill > leftToFill){
filled = leftToFill; //Fill the entire market order
}
else{
filled = availableToFill; //Fill as much as is available in order[i]
}
totalFilled = totalFilled + filled;
_orders[i].filled = _orders[i].filled + filled;
uint cost = filled * _orders[i].price;
if(side_ == Side.BUY){
//Verify that the buyer has enough ETH to cover the purchase (require)
require(balances[msg.sender]["ETH"] >= cost);
//msg.sender is the buyer
balances[msg.sender][ticker_] = balances[msg.sender][ticker_] + filled;
balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"] - cost;
balances[_orders[i].trader][ticker_] = balances[_orders[i].trader][ticker_] - filled;
balances[_orders[i].trader]["ETH"] = balances[_orders[i].trader]["ETH"] + cost;
}
else if(side_ == Side.SELL){
//Msg.sender is the seller
balances[msg.sender][ticker_] = balances[msg.sender][ticker_] - filled;
balances[msg.sender]["ETH"] = balances[msg.sender]["ETH"] + cost;
balances[_orders[i].trader][ticker_] = balances[_orders[i].trader][ticker_] + filled;
balances[_orders[i].trader]["ETH"] = balances[_orders[i].trader]["ETH"] - cost;
}
}
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();
}
}
}
Test
const Dex = artifacts.require("Dex");
const Link = artifacts.require("Link");
const truffleAssert = require("truffle-assertions");
contract("Dex", (accounts) => {
//When creating a SELL market order, the seller needs to have enough tokens for the trade
it("Should throw an error when creating a sell market order without adequate token balance", async () => {
let dex = await Dex.deployed();
let 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)
);
});
//Market orders can be submitted even if the order book is empty
it("Market orders can be submitted even if the order book is empty", async () => {
let dex = await Dex.deployed();
await dex.depositEth({ value: 50000 });
let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 0); //Get buy side orderbook
assert(orderbook.length == 0, "Buy side Orderbook length is not 0");
await truffleAssert.passes(
dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 10)
);
});
//Market orders should be filled until the order book is empty or the market order is 100% filled
it("Market orders should not fill more limit orders than the market order amount", async () => {
let dex = await Dex.deployed();
let link = await Link.deployed();
let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1); //Get sell side orderbook
assert(
orderbook.length == 0,
"Sell side Orderbook should be empty at start of test"
);
await dex.addToken(web3.utils.fromUtf8("LINK"), link.address);
//Send LINK tokens to accounts 1, 2, 3 from account 0
await link.transfer(accounts[1], 150);
await link.transfer(accounts[2], 150);
await link.transfer(accounts[3], 150);
//Approve 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 sell order book
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 market order that should fill 2/3 orders in the book
await 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");
});
//Market orders should be filled until the order book is empty or the market order is 100% filled
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); //Get sell side orderbook
assert(
orderbook.length == 1,
"Sell side Orderbook should have 1 order left"
);
//Fill up the sell order book 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],
});
//check buyer link balance before link purchase
let balanceBefore = await dex.balances(
accounts[0],
web3.utils.fromUtf8("LINK")
);
//Create market order that could fill more than the entire order book (15 link)
await dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 50);
//check buyer link balance after link purchase
let balanceAfter = await dex.balances(
accounts[0],
web3.utils.fromUtf8("LINK")
);
//Buyer should have 15 more link after, even though order was for 50.
assert.equal(balanceBefore.toNumber() + 15, balanceAfter.toNumber());
});
//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();
//Seller deposits link and creates a sell limit order for 1 link for 300 wei
await link.approve(dex.address, 500, { from: accounts[1] });
await dex.createLimitOrder(1, web3.utils.fromUtf8("LINK"), 1, 300, {
from: accounts[1],
});
//Check buyer ETH 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.toNumber() - 300, balanceAfter.toNumber());
});
//The token balances of the limit order sellers should decrease with the filled amounts.
it("The token balances of the limit order sellers should decrease with the filled amounts.", async () => {
let dex = await Dex.deployed();
let link = await Link.deployed();
let orderbook = await dex.getOrderBook(web3.utils.fromUtf8("LINK"), 1); //Get sell side orderbook
assert(
orderbook.length == 0,
"Sell side Orderbook should be empty at start of test"
);
//Seller Account[2] deposits link
await link.approve(dex.address, 500, { from: accounts[2] });
await dex.deposit(100, web3.utils.fromUtf8("LINK"), { from: accounts[2] });
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],
});
//Check sellers Link balances before trade
let account1balanceBefore = await dex.balances(
accounts[1],
web3.utils.fromUtf8("LINK")
);
let account2balanceBefore = await dex.balances(
accounts[2],
web3.utils.fromUtf8("LINK")
);
//Account[0] created market order to buy up both sell orders
await dex.createMarketOrder(0, web3.utils.fromUtf8("LINK"), 2);
//Check sellers Link balances after trade
let account1balanceAfter = await dex.balances(
accounts[1],
web3.utils.fromUtf8("LINK")
);
let account2balanceAfter = await dex.balances(
accounts[2],
web3.utils.fromUtf8("LINK")
);
assert.equal(
account1balanceBefore.toNumber() - 1,
account1balanceAfter.toNumber()
);
assert.equal(
account2balanceBefore.toNumber() - 1,
account2balanceAfter.toNumber()
);
});
//Filled limit orders should be removed from the orderbook
xit("Filled limit orders should be removed from the orderbook", async () => {
let dex = await Dex.deployed();
let link = await Link.deployed();
await dex.addToken(web3.utils.fromUtf8("LINK"), link.address);
//Seller deposits link and creates a sell limit order for 1 link for 300 wei
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); //Get sell side orderbook
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); //Get sell side orderbook
assert(
orderbook.length == 0,
"Sell side Orderbook should be empty after trade"
);
});
//Partly filled limit orders should be 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); //Get sell side orderbook
assert(
orderbook.length == 0,
"Sell side Orderbook should be empty at start of test"
);
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); //Get sell side orderbook
assert.equal(orderbook[0].filled, 2);
assert.equal(orderbook[0].amount, 5);
});
//When creating a BUY market order, the buyer needs to have enough ETH for the trade
it("Should throw an error when creating a buy market order without adequate ETH balance", async () => {
let dex = await Dex.deployed();
let balance = await dex.balances(accounts[4], web3.utils.fromUtf8("ETH"));
assert.equal(balance.toNumber(), 0, "Initial ETH balance is not 0");
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],
})
);
});
});