Skip to content

Commit

Permalink
Orderbook DEX implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
matejos committed Mar 13, 2024
1 parent b7519d0 commit 2012337
Show file tree
Hide file tree
Showing 3 changed files with 298 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";

/// @notice Facilitates base-chain trading of an asset that is living on a different app-chain.
interface IOrderbookDex is IERC165 {
struct Order {
uint256 assetAmount;
uint256 price;
address payable seller;
bool active;
}

/// @notice Returns the current index of orders (index that a new sell order will be mapped to).
function getOrdersIndex() external view returns (uint256);

/// @notice Returns the Order struct information about order of specified `orderId`.
function getOrder(uint256 orderId) external view returns (Order memory);

/// @notice Creates a sell order for the specified `assetAmount` at specified `price`.
/// @dev The order is saved in a mapping from incremental ID to Order struct.
function createSellOrder(uint256 assetAmount, uint256 price) external;

/// @notice Fills an array of orders specified by `orderIds`.
/// @dev Reverts if msg.value is less than the sum of orders' prices.
/// If msg.value is more than the sum of orders' prices, it should refund the difference back to msg.sender.
function fillSellOrders(uint256[] memory orderIds) external payable;

/// @notice Cancels the sell order specified by `orderId`, making it unfillable.
/// @dev Reverts if the msg.sender is not the order's seller.
function cancelSellOrder(uint256 orderId) external;
}
102 changes: 102 additions & 0 deletions packages/contracts/evm-contracts/contracts/orderbook/OrderbookDex.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IOrderbookDex} from "./IOrderbookDex.sol";

/// @notice Facilitates trading an asset that is living on a different app-chain.
contract OrderbookDex is IOrderbookDex, ERC165, ReentrancyGuard {
using Address for address payable;

mapping(uint256 => Order) public orders;
uint256 public ordersIndex;

event OrderCreated(
uint256 indexed orderId,
address indexed seller,
uint256 assetAmount,
uint256 price
);
event OrderFilled(
uint256 indexed orderId,
address indexed seller,
address indexed buyer,
uint256 assetAmount,
uint256 price
);
event OrderCancelled(uint256 indexed orderId);

error OrderIsInactive(uint256 orderId);
error Unauthorized();

/// @notice Returns the current index of orders (index that a new sell order will be mapped to).
function getOrdersIndex() public view returns (uint256) {
return ordersIndex;
}

/// @notice Returns the Order struct information about order of specified `orderId`.
function getOrder(uint256 orderId) public view returns (Order memory) {
return orders[orderId];
}

/// @notice Creates a sell order for the specified `assetAmount` at specified `price`.
/// @dev The order is saved in a mapping from incremental ID to Order struct.
function createSellOrder(uint256 assetAmount, uint256 price) public {
Order memory newOrder = Order({
seller: payable(msg.sender),
assetAmount: assetAmount,
price: price,
active: true
});
orders[ordersIndex] = newOrder;
emit OrderCreated(ordersIndex, msg.sender, assetAmount, price);
ordersIndex++;
}

/// @notice Fills an array of orders specified by `orderIds`.
/// @dev Reverts if msg.value is less than the sum of orders' prices.
/// If msg.value is more than the sum of orders' prices, it should refund the difference back to msg.sender.
function fillSellOrders(uint256[] memory orderIds) public payable nonReentrant {
uint256 length = orderIds.length;
uint256 totalPaid;
for (uint256 i = 0; i < length; ) {
uint256 orderId = orderIds[i];
Order memory order = orders[orderId];
if (!order.active) {
revert OrderIsInactive(orderId);
}
order.seller.sendValue(order.price);
totalPaid += order.price;
orders[orderId].active = false;
emit OrderFilled(orderId, order.seller, msg.sender, order.assetAmount, order.price);
unchecked {
i++;
}
}
if (msg.value > totalPaid) {
payable(msg.sender).sendValue(msg.value - totalPaid);
}
}

/// @notice Cancels the sell order specified by `orderId`, making it unfillable.
/// @dev Reverts if the msg.sender is not the order's seller.
function cancelSellOrder(uint256 orderId) public {
if (msg.sender != orders[orderId].seller) {
revert Unauthorized();
}
orders[orderId].active = false;
emit OrderCancelled(orderId);
}

/// @dev Returns true if this contract implements the interface defined by `interfaceId`. See EIP165.
function supportsInterface(
bytes4 interfaceId
) public view override(ERC165, IERC165) returns (bool) {
return
interfaceId == type(IOrderbookDex).interfaceId || super.supportsInterface(interfaceId);
}
}
162 changes: 162 additions & 0 deletions packages/contracts/evm-contracts/test/OrderbookDex.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import {CheatCodes} from "../test-lib/cheatcodes.sol";
import {CTest} from "../test-lib/ctest.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import {IOrderbookDex} from "../contracts/orderbook/IOrderbookDex.sol";
import {OrderbookDex} from "../contracts/orderbook/OrderbookDex.sol";

contract OrderbookDexTest is CTest {
using Address for address payable;

CheatCodes vm = CheatCodes(HEVM_ADDRESS);
OrderbookDex public dex;
address alice = vm.addr(uint256(keccak256(abi.encodePacked("alice"))));
address boris = vm.addr(uint256(keccak256(abi.encodePacked("boris"))));

function tryToSendEthToDex() public {
payable(address(dex)).sendValue(1);
}

function setUp() public {
dex = new OrderbookDex();
vm.deal(alice, 100 ether);
vm.deal(boris, 100 ether);
}

function test_SupportsInterface() public {
assertTrue(dex.supportsInterface(type(IERC165).interfaceId));
assertTrue(dex.supportsInterface(type(IOrderbookDex).interfaceId));
}

function testFuzz_Fills(uint256 price) public {
uint256 ordersCount = 5;
vm.assume(price < type(uint256).max / ordersCount);
uint256 firstOrderId = dex.getOrdersIndex();
for (uint256 i = 0; i < ordersCount; i++) {
uint256 orderId = dex.getOrdersIndex();
address seller = vm.addr(uint256(keccak256(abi.encodePacked(i))));
uint256 assetAmount = uint256(keccak256(abi.encodePacked(price)));
vm.prank(seller);

vm.expectEmit(true, true, true, true);
emit OrderbookDex.OrderCreated(orderId, seller, assetAmount, price);
dex.createSellOrder(assetAmount, price);

uint256 newOrderId = dex.getOrdersIndex();
OrderbookDex.Order memory order = dex.getOrder(orderId);
assertEq(newOrderId, orderId + 1);
assertTrue(order.active);
assertEq(order.assetAmount, assetAmount);
assertEq(order.price, price);
assertEq(order.seller, seller);
}

{
uint256 currentOrderId = dex.getOrdersIndex();
uint256[] memory orderIds = new uint256[](ordersCount);
for (uint256 i = 0; i < ordersCount; i++) {
orderIds[i] = firstOrderId + i;
}

address buyer = vm.addr(
uint256(keccak256(abi.encodePacked(uint256(type(uint256).max))))
);
vm.deal(buyer, type(uint256).max);
uint256[] memory sellersBalancesBefore = new uint256[](ordersCount);
for (uint256 i = 0; i < ordersCount; i++) {
OrderbookDex.Order memory order = dex.getOrder(firstOrderId + i);
sellersBalancesBefore[i] = order.seller.balance;
vm.expectEmit(true, true, true, true);
emit OrderbookDex.OrderFilled(
firstOrderId + i,
order.seller,
buyer,
order.assetAmount,
order.price
);
}
vm.prank(buyer);
dex.fillSellOrders{value: price * ordersCount}(orderIds);

assertEq(dex.getOrdersIndex(), currentOrderId);
for (uint256 i = 0; i < ordersCount; i++) {
OrderbookDex.Order memory order = dex.getOrder(firstOrderId + i);
assertTrue(!order.active);
assertEq(order.seller.balance, sellersBalancesBefore[i] + order.price);
}
}
}

function test_ExcessValueIsRefunded() public {
uint256 price = 100;
uint256 orderId = dex.getOrdersIndex();
dex.createSellOrder(10, price);

vm.prank(alice);
uint256 aliceBalanceBefore = alice.balance;
uint256[] memory orderIds = new uint256[](1);
orderIds[0] = orderId;
dex.fillSellOrders{value: price * 5}(orderIds);
assertEq(alice.balance, aliceBalanceBefore - price);
}

function test_CannotCancelOrderIfUnauthorized() public {
uint256 orderId = dex.getOrdersIndex();
dex.createSellOrder(100, 200);

vm.prank(alice);
vm.expectRevert(OrderbookDex.Unauthorized.selector);
dex.cancelSellOrder(orderId);
}

function test_CannotFillOrderIfCancelled() public {
uint256 orderId = dex.getOrdersIndex();
dex.createSellOrder(100, 200);
dex.cancelSellOrder(orderId);

vm.prank(alice);
uint256[] memory orderIds = new uint256[](1);
orderIds[0] = orderId;
vm.expectRevert(abi.encodeWithSelector(OrderbookDex.OrderIsInactive.selector, orderId));
dex.fillSellOrders(orderIds);
}

function test_CannotFillOrderIfAlreadyFilled() public {
uint256 orderId = dex.getOrdersIndex();
uint256 price = 1000;
dex.createSellOrder(100, price);

vm.prank(alice);
uint256[] memory orderIds = new uint256[](1);
orderIds[0] = orderId;
dex.fillSellOrders{value: price}(orderIds);

vm.prank(boris);
vm.expectRevert(abi.encodeWithSelector(OrderbookDex.OrderIsInactive.selector, orderId));
dex.fillSellOrders{value: price}(orderIds);
}

function test_CannotFillOrderIfInsufficientValue() public {
uint256 price = 100;
uint256 orderId = dex.getOrdersIndex();
dex.createSellOrder(10, price);

vm.prank(alice);
uint256[] memory orderIds = new uint256[](1);
orderIds[0] = orderId;
vm.expectRevert(
abi.encodeWithSelector(Address.AddressInsufficientBalance.selector, address(dex))
);
dex.fillSellOrders{value: price - 1}(orderIds);
}

function test_CannotSendEtherToDex() public {
vm.expectRevert(Address.FailedInnerCall.selector);
this.tryToSendEthToDex();
}

receive() external payable {}
}

0 comments on commit 2012337

Please sign in to comment.