# Onchain Limit Order Book on Miden VM Miden's [Notes](https://0xpolygonmiden.github.io/miden-base/architecture/notes.html) concept is applied to represent order entities. By utilizing Notes, each with a specific Tag corresponding to an order book, we can seamlessly organize and manage orders ![image](https://hackmd.io/_uploads/ByNbYySna.png) #### Order Creation Configuration users create orders by generating Notes specific to each asset. These Notes encapsulate various types of orders—Market, Limit, and Stop Limit, among others—through embedded parameters like **PRICE**, **BASE_ASSET**, and **QUOTE_ASSET**. Furthermore, we're exploring an innovative approach where a single Note might represent multiple user orders, aiming for a more consolidated and efficient order management system. This idea, while promising, requires deeper development and testing within the testnet environment to identify an optimal solution beyond the current one-order-per-Note strategy. The description and pseudocode for the note logic, currently drafted in Rust, will be updated upon the release of Miden's Rust SDK. ⬇️ ```rust note; //todo figure out how to validate inputs inputs { MAKER: ADDRESS, BASE_ASSET: AssetId, QUOTE_ASSET: AssetId, PRICE: uint256, DIRECTION: String, PRICE_DECIMALS: u8 = 9, // optional MIN_FULFILL_AMOUNT0: u64 = 1, // optional } metadata { TAG: String } enum DIRECTION { BUY: (), SELL: () } struct Arguments { amount: Option<uint256>, } fn main(arguments: Arguments){ let caller = msg_sender(); let payments = msg_payments(); let (maker_payment_asset_id, taker_payment_asset_id) = if argumnts.direction == DIRECTION.SELL { (BASE_ASSET, QUOTE_ASSET) } else { (QUOTE_ASSET, BASE_ASSET) }; if caller == MAKER { //cancel order handler if arguments.amount.is_some() && arguments.amount.unwrap() > 0 && payments.len() == 0 { transfer_to_address(MAKER, maker_payment_asset_id, arguments.amount.unwrap()); } //handler order expand if payments.get(0).is_some() && payments.get(0).unwrap() != maker_payment_asset_id { revert("invalid asset"); } } else{ //fulfill order handler check_fee(payments.get(1).unwrap()); //todo fuigre out how to transfer fee to the matcher let payment = payments.get(0).unwrap(); if payment.asset_id == taker_payment_asset_id { if argumnts.direction == DIRECTION.SELL { //maker asset btc / taker asset usdc let trade_amount = min( convert_quote_to_base(payment.amount), this_balance(BASE_ASSET) ); transfer_to_address(MAKER, QUOTE_ASSET, convert_base_to_quote(trade_amount)); transfer_to_address(caller, BASE_ASSET, trade_amount); }else{ //maker asset usdc / taker asset btc let trade_amount = min( payment.amount, convert_quote_to_base(this_balance(maker_payment_asset_id)) ); transfer_to_address(MAKER, BASE_ASSET, trade_amount); transfer_to_address(caller, QUOTE_ASSET, convert_base_to_quote(trade_amount)); } } } } fn convert_base_to_quote(base_amount: uint256) -> uint256{ let base_asset_decimals = get_native_asset(BASE_ASSET).decimals; let quote_asset_decimals = get_native_asset(QUOTE_ASSET).decimals; base_amount * PRICE / 10.pow(base_asset_decimals + PRICE_DECIMALS - quote_asset_decimals) } fn convert_quote_to_base(quote_amount: uint256) -> uint256 { let base_asset_decimals = get_native_asset(BASE_ASSET).decimals; let quote_asset_decimals = get_native_asset(QUOTE_ASSET).decimals; quote_amount / PRICE * 10.pow(base_asset_decimals + PRICE_DECIMALS - quote_asset_decimals); } fn check_fee(payment: Payment){ //todo fee check logic } ``` - **Limit Order**: Allows users to specify the exact parameters for token exchange, defining the types of tokens desired and their quantities. Upon partial fulfillment, the original Note representing the order is consumed, and a new Note is created for the remaining amount. This ensures that the order can continue to be available for other takers until it's fully completed. The script for this operation might look like the following: ```ts async function createLimitOrder() { const inputs = { MAKER: midenSdk.getProvider().address, BASE_ASSET: assets.BTC.assetId, QUOTE_ASSET: assets.USDC.assetId, PRICE: 71000 * 1e9, DIRECTION: "SELL", } const payment = [ { assetId: assets.BTC.assetId, amount: 1 * 1e8, //1btc }, { assetId: assets.USDC.assetId, amount: sparkSdk.calculateFee(inputs) }, ] const metadata = { tag: "SPARK" } const tx = midenSdk.produceNote("../path_to_stop_limit_note_code", inputs, payment, metadata) await midenSdk.send(tx) } ``` ``` create notes: send(price: get.noteInputs * get.noteArgs, receiver: note.sender) # Sends the agreed amount to the creator of the note create_new_order(vault: 1 ETH * (1 - get.noteArgs), copy.noteInputs, ...) # Creates a new order note with a reduced amount in the vault, copying the initial order's parameters... ``` ![image](https://hackmd.io/_uploads/Sywvn-UCp.png) - **Market Order**: Acts as a taker order, filling existing orders at a specific price. When creating such an order, the price range is calculated, taking into account the slippage tolerance from the volume-weighted average price (market TWAP). All orders from the order book corresponding to this price range are passed to the order creation function as arguments. Then, using the fulfill order function, the market order fully or partially satisfies one or more orders from the arguments. If the orders from the arguments are not enough to fully execute the market order, the remaining funds can be used to create a limit order or returned to the user, depending on the order settings. ```ts async function createMarketOrder() { let inputs = { MAKER: midenSdk.getProvider().address, BASE_ASSET: assets.BTC.assetId, QUOTE_ASSET: assets.USDC.assetId, PRICE: 72000 * 1e9, DIRECTION: "BUY", } const payment = [ { assetId: assets.BTC.assetId, amount: 36000 * 1e6, //0.5 btc }, { assetId: assets.USDC.assetId, amount: sparkSdk.calculateFee(inputs) }, ] const metadata = { tag: "SPARK" } const orderTx = midenSdk.produceNote("../path_to_stop_limit_note_code", inputs, payment, metadata) const orders = await indexerSdk.getMatchingOrdersIds(inputs, payment[0].amount); let matchTxs = sparkSdk.matchOrders(orders, [orderTx.noteId]) await midenSdk.sendTogether([orderTx, ...matchTxs]) //sparkSdk.matchOrders() } ``` Incorporating a feature into the Miden SDK where any amount sent that's less than the balance automatically generates a new note with the remaining balance would simplify transaction logic, similar to Fuel's implementation in predicates. ![image](https://hackmd.io/_uploads/Hk862-U0a.png) - **Stop Limit**: Orders trigger a trade when the asset's market price drops below a specified level. The challenge lies in crafting a note script that adjusts the price for potential execution, initially encrypting inputs and later revealing them for execution. According to documentation (https://polygontechnology.notion.site/How-a-note-in-Miden-can-represent-different-orders-and-why-is-that-useful-ad094634d5a446a8a91863ce6c038136), implementing this off-chain is mentioned, yet the execution method remains unclear. It raises the question: Is an external service required to oversee price changes via an oracle, or can the entire process be managed on-chain without alterations? Ideally, leveraging only the note and node for this functionality would be optimal. However, it's crucial to determine if a note can access an oracle account's state to verify the market price. ```ts // we are going to sell 1btc const inputs = { // configurables baseAsset: assets.BTC.assetId, quoteAsset: assets.USDC.assetId, tag: "SPARK", maker: provider.getAddress(), triggerPrice: 72000 * 1e9, price: 71000 * 1e9, direction: "SELL", } const payment = [ { assetId: assets.BTC.assetId, amount: 1 * 1e8, //1btc }, { assetId: assets.USDC.assetId, amount: sparkSdk.calculateFee(params) }, ] // code of note const note = midenSdk.buildNote("../path_to_stop_limit_note_code", inputs, params, payment) await midenSdk.produceNote(noteHash) } ``` #### Order Cancellation Mechanism: Users interact with a Note, specifying the amount and price (or ID) of the order to be canceled. The system verifies the request, ensuring it originates from the order's owner and that the cancellation does not exceed the order's total cost. Orders can be canceled in whole or in part, with remaining funds transferred to a new Note with identical parameters. But this would require on-chain transaction, maybe there is a way to define logic in script that would cancel this order by reviling the script code when time is up for instance. ```ts aasync function cancelOrder() { const orderId = "<ORDER ID>" const order = await indexerSdk.getOrderDetails(orderId) const args = { amount: 0.5 * 1e8 } const tx = midenSdk.consumeNote(order.noteId, args) await midenSdk.send(tx) } ``` ![image](https://hackmd.io/_uploads/ryvrabICp.png) For market makers, especially during high volatility, efficient order cancellation is crucial as it's a key component of their trading strategies. #### Order Book Visualization The order book will list all **Notes** sharing the same **Tag**, serving as a comprehensive view of the market. This order book will be accessible through a Single Page Application (**SPA**), offering real-time updates and a user-friendly interface for traders. ![image](https://hackmd.io/_uploads/rJUKbPiha.png) #### Matching To efficiently match orders in an order book, use a binary search tree where each node represents a unique price, conwe can taining a list of orders sorted by time . **Buy** and **sell** orders are stored separately to streamline matching. An off-chain service, leveraging Miden's Note DB, organizes orders in a **FIFO** manner, facilitating efficient order matching and execution. No need to store orders in contract because it would make orderbook slow. Exploring three potential mechanisms for order matching: - **Direct Liquidity Provision**: The matcher uses its liquidity to fulfill both sell and buy orders in one go. - **Matching account**: The account acts as an intermediary, executing trades without holding actual assets. It verifies both parties' order terms, absorbing orders and ensuring fair execution with only commission considered. To maintain system trust, the engine's credibility and neutrality are ensured through oversight mechanisms and post-trade verification via zero-knowledge proofs, preventing manipulation. ![image](https://hackmd.io/_uploads/rkREDzUA6.png) #### Questions: - **Simultaneous Payments**: Is it possible to send payments to both the note sender and a protocol account for fees within the same transaction process? - **Oracle-Driven Note Adjustments**: How can notes dynamically adjust their parameters based on oracle market data for efficient order fulfillment? - **Real-Time Order Tracking**: Can we subscribe to NOTES DB changes in Miden to detect newly created orders and automatically integrate them into our buy and sell order tree? - **Retrieving Matched Trades**: Understand the best approach to access records of matched orders (trades), recognizing the necessity for an indexer to display historical trade data akin to Trading View, based on our platform's activities. It's anticipated that Note DB might not be sufficient for this purpose as it likely only retains active notes. - **Off-chain data**: Let's consider the potential of managing some orders off-chain while only submitting proofs of matches or similar validations on-chain. This approach could serve as a robust solution for regulatory compliance, where the protocol can produce zero-knowledge proofs to demonstrate that trades by unauthorized users were not permitted, without disclosing their private information. However, the immediate benefits and specific needs for this functionality remain unclear at this stage. Further investigation will be required to fully understand the advantages and the practical implementation of this concept within our system.