๐ฝJust In Time Liquidity
How we added Just-In-Time Liquidity to regular old orderbooks
What is JIT?
Just in time liquidity is a specific approach to building a CLOB, where no advanced liqudity needs to be locked at the time of order placement. Instead, liquidity is debited "just in time" after matching, at the stage of order finalisation. Crucially, Liquidity is never held locked for any period of time (debits of liquidity and credit of trade tokens always happens in the same transaction).
Design
Implementing Just-in-time liquidity required an overhaul of the internal accounting of orderbooks presently in use. The specific mechanism we deploy requires that the liquidity required for a particular trade must be deposited, however it is not locked. This means that after placing the trade, the user is free to use the same liquidity to place other (JIT) trades, or to withdraw the liquidity, as per their wishes. This design is chosen to balance flexibility of allowing users to use their funds as they wish, with a minimal safeguard against spammy orders (you cannot place a single order for more liquidity than you possess. You can, however, place multiple orders which together exceed your liquidity - however, if matched, some of them may not get filled, unless you supply additional liquidity.)
Note: In the future, users will likely be required to leave a small deposit locked in the vault for each trade they create - approximately 0.2% of the trade value. This is intended to incentivise traders to close out trades that they no longer intend to fill, in time. It can also be used to compensate counterparties, in case of a failed trade.
Workflow modifications
CreateOrder Like typical CLOBs, Fermi checks if there are sufficient deposits to open a trade - however, if deposits are insufficient, Fermi simply approves tokens instead of transferring them upfront. FinaliseMatch
Once any portion of an order has been filled, the information about the matched trades will be pushed to the EventQ, with trade quantities of both tokens involved. Upon invoking FinaliseMatch
, async transfers of liqudity from both counterparties take place. However, crucially, the deposit is credited to the unlocked token balance of the user, instead of the locked token balance. This enables the same liquidity to be available across trades, and for the liqudity to be withdrawable if the other counterparty fails to provide liquidity.
Once both parties have supplied liquidity, internal accounting changes representing JIT are executed. If successful, both users find the desired tokens credited to their market balances.
How FinaliseMatch works
Reminder: User specific information about locked and unlocked token balances is held in the OpenOrders Account, which is a unique PDA for every user of the market. It contains accounting information which is changed when orders are finalised. It is used as a source of ground truth when processing withdrawals.
FinaliseMatch
is invoked when a match has been found for an order placed earlier. Matched orders can be discovered by parsing the EventQ.
This step enables the "Just In Time" liquidity feature of Fermi DEX.
1. Execution
To execute this on a particular order, the following arguments are needed:
owner_slot: u8,
cpty_event_slot: u8,
orderId: u128,
authority_cpty: Pubkey,
owner: Pubkey,
owner_side: Side,
in addition, the following accounts must be passed (this is handled automatically by our frontend, however it is relevant if interacting via CLI):
Standard Accounts:
MarketPDA,coinMint, pcMint, coinVault, pcVault, eventQ
And the all important finalize-specific accounts, used for making the token balance modifications for the counterparties:
OpenOrdersMaker
OpenOrdersTaker
Safety checks
The function checks that the provided events form a bid-ask pair for the same order_id, and that the event flag is set to fill, and trade values are non-zero. Further, it checks that the openordersmaker and openorderstaker PDA's provided are correct and correspond to the owners in the events provided. This sanity check ensures no malicious manipulation of other users' OpenOrders PDA.
Logic
Both trade related events are processed, one at a time. While processing each event, the balance paid by the maker/taker is debited from their corresponding OpenOrders account, and the balance of the other token in the pair, is credited in their OpenOrders account. Once this process succeeds for both counterparties, an "orderfinalised" event is emitted to log the completion of this order, and to prevent double execution. In case the balance debit fails for either of the parties, the transaction reverts, with no changes to any balances. In that event, the matched order remains open to being finalised in the future.
Replay Attacks
To prevent replay attacks, where the same trade is "finalised" more than once, we modify the eventQ in place, changing the finalised flag for that particular order from 0
to 1
Finalise Match example
An example for how to use the atomic FinaliseMatch Functionality is provided in the tests folder (see testing locally to replicate this). With the correct arguments and accounts specified, calling FinaliseMatch
executes the swap of 25 USDC (PC tokens) for 1 SOL (coins). The internal token balances of each counterparty are adjusted "just in time" at this stage.
See an Example Finalise Transaction here: https://solscan.io/tx/2bSohxq73L9gouc4dmuQnkkd1uctNxMuNw2xmJms1RntvUNb5cdWt7NqMmpujhjPTGLZFMZxzFqVEJcyAzLGx1uH?cluster=devnet
You can verify this executes as expected by inspecting the logs.
Last updated