Skip to content

Commit

Permalink
Identify order by a singular id (seller+sellerNonce packed), add docu…
Browse files Browse the repository at this point in the history
…mentation
  • Loading branch information
matejos committed Apr 4, 2024
1 parent d3a0212 commit 782d413
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 148 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,71 +5,86 @@ pragma solidity ^0.8.20;
import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";

/// @notice Facilitates base-chain trading of an asset that is living on a different app-chain.
/// @dev The contract should never hold any ETH itself.
interface IOrderbookDex is IERC1155Receiver {
struct Order {
/// @dev The asset's unique token identifier.
uint256 assetId;
/// @dev The amount of the asset that is available to be sold.
uint256 assetAmount;
/// @dev The price per one unit of asset.
uint256 pricePerAsset;
}

/// @param seller The seller's address.
/// @param orderId The order's unique identifier.
/// @param assetId The asset's unique token identifier.
/// @param assetAmount The amount of the asset that has been put for sale.
/// @param pricePerAsset The requested price per one unit of asset.
event OrderCreated(
address indexed seller,
uint256 indexed orderId,
uint256 indexed assetId,
uint256 assetAmount,
uint256 pricePerAsset
);

/// @param seller The seller's address.
/// @param orderId The order's unique identifier.
/// @param buyer The buyer's address.
/// @param assetAmount The amount of the asset that was traded.
/// @param pricePerAsset The price per one unit of asset that was paid.
event OrderFilled(
address indexed seller,
uint256 indexed orderId,
address indexed buyer,
uint256 assetAmount,
uint256 pricePerAsset
);

/// @param seller The seller's address.
/// @param orderId The order's unique identifier.
event OrderCancelled(address indexed seller, uint256 indexed orderId);

/// @notice Returns the address of the asset that is being traded.
/// @notice Returns the address of the asset that is being traded in this DEX contract.
function getAsset() external view returns (address);

/// @notice Returns the seller's current `orderId` (index that their new sell order will be mapped to).
function getSellerOrderId(address seller) external view returns (uint256);
/// @notice Returns the `orderId` of the next sell order of `seller`.
function getNextOrderId(address seller) external view returns (uint256);

/// @notice Returns the Order struct information about an order identified by the combination `<seller, orderId>`.
function getOrder(address seller, uint256 orderId) external view returns (Order memory);
/// @notice Returns the Order struct information about an order identified by the `orderId`.
function getOrder(uint256 orderId) external view returns (Order memory);

/// @notice Creates a sell order with incremental seller-specific `orderId` for the specified `assetAmount` at specified `pricePerAsset`.
/// @dev The order information is saved in a nested mapping `seller address -> orderId -> Order`.
/// @notice Creates a sell order for the `assetAmount` of `assetId` at `pricePerAsset`.
/// @dev The order information is saved in a mapping `orderId -> Order`.
/// orderId SHOULD be created by packing the seller's address (uint160) and their incremental `sellerOrderNonce` (uint96) into uint256.
/// MUST transfer the `assetAmount` of `assetId` from the seller to the contract.
/// MUST emit `OrderCreated` event.
function createSellOrder(uint256 assetId, uint256 assetAmount, uint256 pricePerAsset) external;

/// @notice Consecutively fills an array of orders identified by the combination `<seller, orderId>`,
/// @notice Consecutively fills an array of orders identified by the `orderId` of each order,
/// by providing an exact amount of ETH and requesting a specific minimum amount of asset to receive.
/// @dev Transfers portions of msg.value to the orders' sellers according to the price.
/// The sum of asset amounts of filled orders MUST be at least `minimumAsset`.
/// If msg.value is more than the sum of orders' prices, it MUST refund the excess back to msg.sender.
/// MUST change the `assetAmount` parameter for the specified order according to how much of it was filled.
/// MUST decrease the `assetAmount` parameter for the specified order according to how much of it was filled,
/// and transfer that amount of the order's `assetId` to the buyer.
/// MUST emit `OrderFilled` event for each order accordingly.
function fillOrdersExactEth(
uint256 minimumAsset,
address payable[] memory sellers,
uint256[] memory orderIds
) external payable;
function fillOrdersExactEth(uint256 minimumAsset, uint256[] memory orderIds) external payable;

/// @notice Consecutively fills an array of orders identified by the combination `<seller, orderId>`,
/// @notice Consecutively fills an array of orders identified by the `orderId` of each order,
/// by providing a possibly surplus amount of ETH and requesting an exact amount of asset to receive.
/// @dev Transfers portions of msg.value to the orders' sellers according to the price.
/// The sum of asset amounts of filled orders MUST be exactly `assetAmount`. Excess ETH MUST be returned back to `msg.sender`.
/// MUST change the `assetAmount` parameter for the specified order according to how much of it was filled.
/// MUST decrease the `assetAmount` parameter for the specified order according to how much of it was filled,
/// and transfer that amount of the order's `assetId` to the buyer.
/// MUST emit `OrderFilled` event for each order accordingly.
/// If msg.value is more than the sum of orders' prices, it MUST refund the difference back to msg.sender.
function fillOrdersExactAsset(
uint256 assetAmount,
address payable[] memory sellers,
uint256[] memory orderIds
) external payable;
function fillOrdersExactAsset(uint256 assetAmount, uint256[] memory orderIds) external payable;

/// @notice Cancels the sell order identified by combination `<msg.sender, orderId>`, making it unfillable.
/// @dev MUST change the `assetAmount` parameter for the specified order to `0`.
/// @notice Cancels the sell order identified by the `orderId`, transferring the order's assets back to the seller.
/// @dev MUST revert if the seller decoded from the `orderId` is not `msg.sender`.
/// MUST change the `assetAmount` parameter for the specified order to `0`.
/// MUST emit `OrderCancelled` event.
/// MUST transfer the `assetAmount` of `assetId` back to the seller.
function cancelSellOrder(uint256 orderId) external;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,40 +10,42 @@ import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol
import {IInverseProjected1155} from "../token/IInverseProjected1155.sol";
import {IOrderbookDex} from "./IOrderbookDex.sol";

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

IInverseProjected1155 internal asset;
mapping(address => mapping(uint256 => Order)) internal orders;
mapping(address => uint256) internal sellersOrderId;
mapping(uint256 orderId => Order) internal orders;
mapping(address seller => uint96 nonce) internal sellersOrderNonce;

error OrderDoesNotExist(uint256 orderId);
error InsufficientEndAmount(uint256 expectedAmount, uint256 actualAmount);
error InvalidInput(uint256 input);
error InvalidInputArity();
error Unauthorized();

constructor(IInverseProjected1155 _asset) {
asset = _asset;
}

/// @notice Returns the address of the asset that is being traded.
/// @notice Returns the address of the asset that is being traded in this DEX contract.
function getAsset() public view virtual returns (address) {
return address(asset);
}

/// @notice Returns the seller's current `orderId` (index that their new sell order will be mapped to).
function getSellerOrderId(address seller) public view virtual returns (uint256) {
return sellersOrderId[seller];
/// @notice Returns the `orderId` of the next sell order of `seller`.
function getNextOrderId(address seller) public view virtual returns (uint256) {
return _getOrderId(seller, sellersOrderNonce[seller]);
}

/// @notice Returns the Order struct information about an order identified by the combination `<seller, orderId>`.
function getOrder(address seller, uint256 orderId) public view virtual returns (Order memory) {
return orders[seller][orderId];
/// @notice Returns the Order struct information about an order identified by the `orderId`.
function getOrder(uint256 orderId) public view virtual returns (Order memory) {
return orders[orderId];
}

/// @notice Creates a sell order with incremental seller-specific `orderId` for the specified `assetAmount` at specified `pricePerAsset`.
/// @dev The order information is saved in a nested mapping `seller address -> orderId -> Order`.
/// @notice Creates a sell order for the `assetAmount` of `assetId` at `pricePerAsset`.
/// @dev The order information is saved in a mapping `orderId -> Order`.
/// orderId SHOULD be created by packing the seller's address (uint160) and their incremental `sellerOrderNonce` (uint96) into uint256.
/// MUST transfer the `assetAmount` of `assetId` from the seller to the contract.
/// MUST emit `OrderCreated` event.
function createSellOrder(
uint256 assetId,
Expand All @@ -59,45 +61,41 @@ contract OrderbookDex is IOrderbookDex, ERC1155Holder, ReentrancyGuard {
assetAmount: assetAmount,
pricePerAsset: pricePerAsset
});
uint256 orderId = sellersOrderId[msg.sender];
orders[msg.sender][orderId] = newOrder;
emit OrderCreated(msg.sender, orderId, assetAmount, pricePerAsset);
sellersOrderId[msg.sender]++;
uint256 orderId = getNextOrderId(msg.sender);
orders[orderId] = newOrder;
emit OrderCreated(msg.sender, orderId, assetId, assetAmount, pricePerAsset);
sellersOrderNonce[msg.sender]++;
}

/// @notice Consecutively fills an array of orders identified by the combination `<seller, orderId>`,
/// @notice Consecutively fills an array of orders identified by the `orderId` of each order,
/// by providing an exact amount of ETH and requesting a specific minimum amount of asset to receive.
/// @dev Transfers portions of msg.value to the orders' sellers according to the price.
/// The sum of asset amounts of filled orders MUST be at least `minimumAsset`.
/// If msg.value is more than the sum of orders' prices, it MUST refund the excess back to msg.sender.
/// MUST change the `assetAmount` parameter for the specified order according to how much of it was filled.
/// MUST decrease the `assetAmount` parameter for the specified order according to how much of it was filled,
/// and transfer that amount of the order's `assetId` to the buyer.
/// MUST emit `OrderFilled` event for each order accordingly.
function fillOrdersExactEth(
uint256 minimumAsset,
address payable[] memory sellers,
uint256[] memory orderIds
) public payable virtual nonReentrant {
if (sellers.length != orderIds.length) {
revert InvalidInputArity();
}
uint256 length = sellers.length;
uint256 length = orderIds.length;
uint256 remainingEth = msg.value;
uint256 totalAssetReceived;
for (uint256 i = 0; i < length; i++) {
address payable seller = sellers[i];
uint256 orderId = orderIds[i];
Order storage order = orders[seller][orderId];
Order storage order = orders[orderId];
if (order.assetAmount == 0) {
continue;
}
address payable seller = payable(address(uint160(orderId >> 96)));
uint256 assetsToBuy = remainingEth / order.pricePerAsset;
if (assetsToBuy == 0) {
continue;
}
if (assetsToBuy > order.assetAmount) {
assetsToBuy = order.assetAmount;
}
seller.sendValue(assetsToBuy * order.pricePerAsset);
order.assetAmount -= assetsToBuy;
remainingEth -= assetsToBuy * order.pricePerAsset;
totalAssetReceived += assetsToBuy;
Expand All @@ -108,6 +106,7 @@ contract OrderbookDex is IOrderbookDex, ERC1155Holder, ReentrancyGuard {
assetsToBuy,
bytes("")
);
seller.sendValue(assetsToBuy * order.pricePerAsset);
emit OrderFilled(seller, orderId, msg.sender, assetsToBuy, order.pricePerAsset);
if (remainingEth == 0) {
break;
Expand All @@ -121,39 +120,35 @@ contract OrderbookDex is IOrderbookDex, ERC1155Holder, ReentrancyGuard {
}
}

/// @notice Consecutively fills an array of orders identified by the combination `<seller, orderId>`,
/// @notice Consecutively fills an array of orders identified by the `orderId` of each order,
/// by providing a possibly surplus amount of ETH and requesting an exact amount of asset to receive.
/// @dev Transfers portions of msg.value to the orders' sellers according to the price.
/// The sum of asset amounts of filled orders MUST be exactly `assetAmount`. Excess ETH MUST be returned back to `msg.sender`.
/// MUST change the `assetAmount` parameter for the specified order according to how much of it was filled.
/// MUST decrease the `assetAmount` parameter for the specified order according to how much of it was filled,
/// and transfer that amount of the order's `assetId` to the buyer.
/// MUST emit `OrderFilled` event for each order accordingly.
/// If msg.value is more than the sum of orders' prices, it MUST refund the difference back to msg.sender.
function fillOrdersExactAsset(
uint256 assetAmount,
address payable[] memory sellers,
uint256[] memory orderIds
) public payable virtual nonReentrant {
if (sellers.length != orderIds.length) {
revert InvalidInputArity();
}
uint256 length = sellers.length;
uint256 length = orderIds.length;
uint256 remainingAsset = assetAmount;
uint256 remainingEth = msg.value;
for (uint256 i = 0; i < length; i++) {
address payable seller = sellers[i];
uint256 orderId = orderIds[i];
Order storage order = orders[seller][orderId];
Order storage order = orders[orderId];
if (order.assetAmount == 0) {
continue;
}
address payable seller = payable(address(uint160(orderId >> 96)));
uint256 assetsToBuy = order.assetAmount;
if (assetsToBuy > remainingAsset) {
assetsToBuy = remainingAsset;
}
if (assetsToBuy == 0) {
continue;
}
seller.sendValue(assetsToBuy * order.pricePerAsset);
order.assetAmount -= assetsToBuy;
remainingEth -= assetsToBuy * order.pricePerAsset;
remainingAsset -= assetsToBuy;
Expand All @@ -164,6 +159,7 @@ contract OrderbookDex is IOrderbookDex, ERC1155Holder, ReentrancyGuard {
assetsToBuy,
bytes("")
);
seller.sendValue(assetsToBuy * order.pricePerAsset);
emit OrderFilled(seller, orderId, msg.sender, assetsToBuy, order.pricePerAsset);
if (remainingAsset == 0) {
break;
Expand All @@ -177,11 +173,17 @@ contract OrderbookDex is IOrderbookDex, ERC1155Holder, ReentrancyGuard {
}
}

/// @notice Cancels the sell order identified by combination `<msg.sender, orderId>`, making it unfillable.
/// @dev MUST change the `assetAmount` parameter for the specified order to `0`.
/// @notice Cancels the sell order identified by the `orderId`, transferring the order's assets back to the seller.
/// @dev MUST revert if the seller decoded from the `orderId` is not `msg.sender`.
/// MUST change the `assetAmount` parameter for the specified order to `0`.
/// MUST emit `OrderCancelled` event.
/// MUST transfer the `assetAmount` of `assetId` back to the seller.
function cancelSellOrder(uint256 orderId) public virtual {
Order storage order = orders[msg.sender][orderId];
address seller = address(uint160(orderId >> 96));
if (msg.sender != seller) {
revert Unauthorized();
}
Order storage order = orders[orderId];
uint256 assetAmount = order.assetAmount;
if (order.assetAmount == 0) {
revert OrderDoesNotExist(orderId);
Expand All @@ -198,4 +200,8 @@ contract OrderbookDex is IOrderbookDex, ERC1155Holder, ReentrancyGuard {
return
interfaceId == type(IOrderbookDex).interfaceId || super.supportsInterface(interfaceId);
}

function _getOrderId(address seller, uint96 orderId) internal view virtual returns (uint256) {
return (uint256(uint160(seller)) << 96) ^ orderId;
}
}
Loading

0 comments on commit 782d413

Please sign in to comment.