-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
298 additions
and
0 deletions.
There are no files selected for viewing
34 changes: 34 additions & 0 deletions
34
packages/contracts/evm-contracts/contracts/orderbook/IOrderbookDex.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
102
packages/contracts/evm-contracts/contracts/orderbook/OrderbookDex.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
162
packages/contracts/evm-contracts/test/OrderbookDex.t.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} | ||
} |