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