Hey folks ,
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));
});
});