/ smart-contract / contracts / OrderBook.sol
OrderBook.sol
  1  // SPDX-License-Identifier: UNLICENSED
  2  pragma solidity ^0.8.28;
  3  
  4  contract OrderBook {
  5      struct Order {
  6          address user;
  7          bool isBuy;       // true: Buy order, false: Sell order
  8          uint256 price;    // Price in 1e18 units (allows decimals)
  9          uint256 quantity; // Integer quantity (shares)
 10          bool active;      // Whether the order is still active
 11      }
 12  
 13      // Tracks each user's balance per security (securityId => balance)
 14      mapping(address => mapping(uint256 => int256)) public balances;
 15  
 16      // Orders per security (securityId => array of orders)
 17      mapping(uint256 => Order[]) public orders;
 18  
 19      // Owner (deployer) for seeding
 20      address public owner;
 21  
 22      // Events
 23      event OrderPlaced(
 24          address indexed user,
 25          uint256 indexed securityId,
 26          bool isBuy,
 27          uint256 price,
 28          uint256 quantity,
 29          uint256 orderId
 30      );
 31      event OrderMatched(
 32          address indexed buyer,
 33          address indexed seller,
 34          uint256 indexed securityId,
 35          uint256 price,
 36          uint256 quantity
 37      );
 38      event OrderCancelled(
 39          address indexed user,
 40          uint256 indexed securityId,
 41          uint256 orderId
 42      );
 43  
 44      constructor() {
 45          owner = msg.sender;
 46      }
 47  
 48      /// @notice Returns the total number of orders for a security
 49      function getOrderCount(uint256 _securityId) external view returns (uint256) {
 50          return orders[_securityId].length;
 51      }
 52  
 53      /// @notice Returns details for a given order ID in a security
 54      function getOrder(uint256 _securityId, uint256 _orderId)
 55          external
 56          view
 57          returns (
 58              address user,
 59              bool isBuy,
 60              uint256 price,
 61              uint256 quantity,
 62              bool active
 63          )
 64      {
 65          require(_orderId < orders[_securityId].length, "Order ID out of range");
 66          Order memory o = orders[_securityId][_orderId];
 67          return (o.user, o.isBuy, o.price, o.quantity, o.active);
 68      }
 69  
 70      /// @notice Returns the balance for a user and security
 71      function getBalance(address _user, uint256 _securityId) external view returns (int256) {
 72          return balances[_user][_securityId];
 73      }
 74  
 75      /// @notice Places a new order and matches against existing orders
 76      function placeOrder(uint256 _securityId, bool _isBuy, uint256 _price, uint256 _quantity) external {
 77          require(_price > 0, "Price must be > 0");
 78          require(_quantity > 0, "Quantity must be > 0");
 79  
 80          uint256 leftover = _quantity;
 81  
 82          if (_isBuy) {
 83              // Match with SELL orders <= _price
 84              while (leftover > 0) {
 85                  int256 bestIndex = -1;
 86                  uint256 bestPrice = type(uint256).max;
 87  
 88                  for (uint256 i = 0; i < orders[_securityId].length; i++) {
 89                      Order storage o = orders[_securityId][i];
 90                      if (!o.active || o.isBuy) continue;
 91                      if (o.price <= _price && o.price < bestPrice) {
 92                          bestPrice = o.price;
 93                          bestIndex = int256(i);
 94                      }
 95                  }
 96  
 97                  if (bestIndex < 0) break;
 98  
 99                  Order storage matchOrder = orders[_securityId][uint256(bestIndex)];
100                  uint256 matchQty = (matchOrder.quantity < leftover) ? matchOrder.quantity : leftover;
101  
102                  leftover -= matchQty;
103                  matchOrder.quantity -= matchQty;
104  
105                  balances[msg.sender][_securityId] += int256(matchQty);
106                  balances[matchOrder.user][_securityId] -= int256(matchQty);
107  
108                  emit OrderMatched(msg.sender, matchOrder.user, _securityId, matchOrder.price, matchQty);
109  
110                  if (matchOrder.quantity == 0) {
111                      matchOrder.active = false;
112                  }
113              }
114  
115              // Post unmatched as new BUY order
116              if (leftover > 0) {
117                  uint256 orderId = orders[_securityId].length;
118                  orders[_securityId].push(Order(msg.sender, true, _price, leftover, true));
119                  emit OrderPlaced(msg.sender, _securityId, true, _price, leftover, orderId);
120              }
121          } else {
122              // Match with BUY orders >= _price
123              while (leftover > 0) {
124                  int256 bestIndex = -1;
125                  uint256 bestPrice = 0;
126  
127                  for (uint256 i = 0; i < orders[_securityId].length; i++) {
128                      Order storage o = orders[_securityId][i];
129                      if (!o.active || !o.isBuy) continue;
130                      if (o.price >= _price && o.price > bestPrice) {
131                          bestPrice = o.price;
132                          bestIndex = int256(i);
133                      }
134                  }
135  
136                  if (bestIndex < 0) break;
137  
138                  Order storage matchOrder = orders[_securityId][uint256(bestIndex)];
139                  uint256 matchQty = (matchOrder.quantity < leftover) ? matchOrder.quantity : leftover;
140  
141                  leftover -= matchQty;
142                  matchOrder.quantity -= matchQty;
143  
144                  balances[matchOrder.user][_securityId] += int256(matchQty);
145                  balances[msg.sender][_securityId] -= int256(matchQty);
146  
147                  emit OrderMatched(matchOrder.user, msg.sender, _securityId, matchOrder.price, matchQty);
148  
149                  if (matchOrder.quantity == 0) {
150                      matchOrder.active = false;
151                  }
152              }
153  
154              // Post unmatched as new SELL order
155              if (leftover > 0) {
156                  uint256 orderId = orders[_securityId].length;
157                  orders[_securityId].push(Order(msg.sender, false, _price, leftover, true));
158                  emit OrderPlaced(msg.sender, _securityId, false, _price, leftover, orderId);
159              }
160          }
161      }
162  
163      /// @notice Cancels a single active order
164      function cancelOrder(uint256 _securityId, uint256 _orderId) external {
165          require(_orderId < orders[_securityId].length, "Order ID out of range");
166          Order storage order = orders[_securityId][_orderId];
167          require(order.active, "Order is not active");
168          require(order.user == msg.sender, "Not order owner");
169          order.active = false;
170          emit OrderCancelled(msg.sender, _securityId, _orderId);
171      }
172  
173      /// @notice Cancels all active orders for a security by the caller
174      function cancelAllOrders(uint256 _securityId) external {
175          for (uint256 i = 0; i < orders[_securityId].length; i++) {
176              if (orders[_securityId][i].active && orders[_securityId][i].user == msg.sender) {
177                  orders[_securityId][i].active = false;
178                  emit OrderCancelled(msg.sender, _securityId, i);
179              }
180          }
181      }
182  
183      /// @notice Seeds a sample order for a security
184      function seedOrder(
185          address _user,
186          uint256 _securityId,
187          bool _isBuy,
188          uint256 _price,
189          uint256 _quantity
190      ) external {
191          require(msg.sender == owner, "Only owner can seed orders");
192          require(_price > 0, "Price must be > 0");
193          require(_quantity > 0, "Quantity must be > 0");
194  
195          uint256 orderId = orders[_securityId].length;
196          orders[_securityId].push(Order(_user, _isBuy, _price, _quantity, true));
197          emit OrderPlaced(_user, _securityId, _isBuy, _price, _quantity, orderId);
198      }
199  }
200  
201  
202