IOrderBookV1

Git Source

Inherits: IERC3156FlashLender, IInterpreterCallerV1

An orderbook that deploys strategies represented as interpreter expressions rather than individual orders. The order book contract itself behaves similarly to an ERC4626 vault but with much more fine grained control over how tokens are allocated and moved internally by their owners, and without any concept of "shares". Token owners MAY deposit and withdraw their tokens under arbitrary vault IDs on a per-token basis, then define orders that specify how tokens move between vaults according to an expression. The expression returns a maximum amount and a token input/output ratio from the perpective of the order. When two expressions intersect, as in their ratios are the inverse of each other, then tokens can move between vaults. For example, consider order A with input TKNA and output TKNB with a constant ratio of 100:1. This order in isolation has no ability to move tokens. If an order B appears with input TKNB and output TKNA and a ratio of 1:100 then this is a perfect match with order A. In this case 100 TKNA will move from order B to order A and 1 TKNB will move from order A to order B. IO ratios are always specified as input:output and are 18 decimal fixed point values. The maximum amount that can be moved in the current clearance is also set by the order expression as an 18 decimal fixed point value. Typically orders will not clear when their match is exactly 1:1 as the clearer needs to pay gas to process the match. Each order will get exactly the ratio it calculates when it does clear so if there is overlap in the ratios then the clearer keeps the difference. In our above example, consider order B asking a ratio of 1:110 instead of 1:100. In this case 100 TKNA will move from order B to order A and 10 TKNA will move to the clearer's vault and 1 TKNB will move from order A to order B. In the case of fixed prices this is not very interesting as order B could more simply take order A directly for cheaper rather than involving a third party. Indeed, Orderbook supports a direct "take orders" method that works similar to a "market buy". In the case of dynamic expression based ratios, it allows both order A and order B to clear non-interactively according to their strategy, trading off active management, dealing with front-running, MEV, etc. for zero-gas and exact-ratio clearance. The general invariant for clearing and take orders is:

ratioA = InputA / OutputA
ratioB = InputB / OutputB
ratioA * ratioB = ( InputA * InputB ) / ( OutputA * OutputB )
OutputA >= InputB
OutputB >= InputA
∴ ratioA * ratioB <= 1

Orderbook is IERC3156FlashLender compliant with a 0 fee flash loan implementation to allow external liquidity from other onchain DEXes to match against orderbook expressions. All deposited tokens across all vaults are available for flashloan, the flashloan MAY BE REPAID BY CALLING TAKE ORDER such that Orderbook's liability to its vaults is decreased by an incoming trade from the flashloan borrower. See ZeroExOrderBookFlashBorrower for an example of how this works in practise. Orderbook supports many to many input/output token relationship, for example some order can specify an array of stables it would be willing to accept in return for some ETH. This removes the need for a combinatorial explosion of order strategies between like assets but introduces the issue of token decimal handling. End users understand that "one" USDT is roughly equal to "one" DAI, but onchain this is incorrect by 12 orders of magnitude. This is because "one" DAI is 1e18 tokens and "one" USDT is 1e6 tokens. The orderbook is allowing orders to deploy expressions that define economic equivalence but this doesn't map 1:1 with numeric equivalence in a many to many setup behind token decimal convensions. The solution is to require that end users who place orders provide the decimals of each token they include in their valid IO lists, and to calculate all amounts and ratios in their expressions as though they were 18 decimal fixed point values. Orderbook will then automatically rescale the expression values before applying the final vault movements. If an order provides the "wrong" decimal values for some token then it will simply calculate its own ratios and amounts incorrectly which will either lead to no matching orders or a very bad trade for the order owner. There is no way that misrepresenting decimals can attack some other order by a counterparty. Orderbook DOES NOT read decimals from tokens onchain because A. this would be gas for an external call to a cold token contract and B. the ERC20 standard specifically states NOT to read decimals from the interface onchain. Token amounts and ratios returned by calculate order MUST be 18 decimal fixed point values. Token amounts input to handle IO MUST be the exact absolute values that move between the vaults, i.e. NOT rescaled to 18 decimals. The author of the handle IO expression MUST use the token decimals and amounts to rescale themselves if they want that logic, notably the expression author will need to specify the desired rounding behaviour in the rescaling process. When two orders clear there are NO TOKEN MOVEMENTS, only internal vault balances are updated from the input and output vaults. Typically this results in less gas per clear than calling external token transfers and also avoids issues with reentrancy, allowances, external balances etc. This also means that REBASING TOKENS AND TOKENS WITH DYNAMIC BALANCE ARE NOT SUPPORTED. Orderbook ONLY WORKS IF TOKEN BALANCES ARE 1:1 WITH ADDITION/SUBTRACTION PER VAULT MOVEMENT. Dust due to rounding errors always favours the order. Output max is rounded down and IO ratios are rounded up. Input and output amounts are always converted to absolute values before applying to vault balances such that orderbook always retains fully collateralised inventory of underlying token balances to support withdrawals, with the caveat that dynamic token balanes are not supported. When an order clears it is NOT removed. Orders remain active until the owner deactivates them. This is gas efficient as order owners MAY deposit more tokens in a vault with an order against it many times and the order strategy will continue to be clearable according to its expression. As vault IDs are uint256 values there are effectively infinite possible vaults for any token so there is no limit to how many active orders any address can have at one time. This also allows orders to be daisy chained arbitrarily where output vaults for some order are the input vaults for some other order. Expression storage is namespaced by order owner, so gets and sets are unique to each onchain address. Order owners MUST TAKE CARE not to override their storage sets globally across all their orders, which they can do most simply by hashing the order hash into their get/set keys inside the expression. This gives maximum flexibility for shared state across orders without allowing order owners to attack and overwrite values stored by orders placed by their counterparty. Note that each order specifies its own interpreter and deployer so the owner is responsible for not corrupting their own calculations with bad interpreters. This also means the Orderbook MUST assume the interpreter, and notably the interpreter's store, is malicious and guard against reentrancy etc. As Orderbook supports any expression that can run on any IInterpreterV1 and counterparties are available to the order, order strategies are free to implement KYC/membership, tracking, distributions, stock, buybacks, etc. etc.

Functions

vaultBalance

Get the current balance of a vault for a given owner, token and vault ID.

function vaultBalance(address owner, address token, uint256 id) external view returns (uint256 balance);

Parameters

NameTypeDescription
owneraddressThe owner of the vault.
tokenaddressThe token the vault is for.
iduint256The vault ID to read.

Returns

NameTypeDescription
balanceuint256The current balance of the vault.

deposit

msg.sender deposits tokens according to config. The config specifies the vault to deposit tokens under. Delegated depositing is NOT supported. Depositing DOES NOT mint shares (unlike ERC4626) so the overall vaulted experience is much simpler as there is always a 1:1 relationship between deposited assets and vault balances globally and individually. This mitigates rounding/dust issues, speculative behaviour on derived assets, possible regulatory issues re: whether a vault share is a security, code bloat on the vault, complex mint/deposit/withdraw/redeem 4-way logic, the need for preview functions, etc. etc. At the same time, allowing vault IDs to be specified by the depositor allows much more granular and direct control over token movements within Orderbook than either ERC4626 vault shares or mere contract-level ERC20 allowances can facilitate.

function deposit(DepositConfig calldata config) external;

Parameters

NameTypeDescription
configDepositConfigAll config for the deposit.

withdraw

Allows the sender to withdraw any tokens from their own vaults. If the withrawer has an active flash loan debt denominated in the same token being withdrawn then Orderbook will merely reduce the debt and NOT send the amount of tokens repaid to the flashloan debt.

function withdraw(WithdrawConfig calldata config) external;

Parameters

NameTypeDescription
configWithdrawConfigAll config required to withdraw. Notably if the amount is less than the current vault balance then the vault will be cleared to 0 rather than the withdraw transaction reverting.

addOrder

Given an order config, deploys the expression and builds the full Order for the config, then records it as an active order. Delegated adding an order is NOT supported. The msg.sender that adds an order is ALWAYS the owner and all resulting vault movements are their own.

function addOrder(OrderConfig calldata config) external;

Parameters

NameTypeDescription
configOrderConfigAll config required to build an Order.

removeOrder

Order owner can remove their own orders. Delegated order removal is NOT supported and will revert. Removing an order multiple times or removing an order that never existed are valid, the event will be emitted and the transaction will complete with that order hash definitely, redundantly not live.

function removeOrder(Order calldata order) external;

Parameters

NameTypeDescription
orderOrderThe Order data exactly as it was added.

takeOrders

Allows msg.sender to attempt to fill a list of orders in sequence without needing to place their own order and clear them. This works like a market buy but against a specific set of orders. Every order will looped over and calculated individually then filled maximally until the request input is reached for the msg.sender. The msg.sender is responsible for selecting the best orders at the time according to their criteria and MAY specify a maximum IO ratio to guard against an order spiking the ratio beyond what the msg.sender expected and is comfortable with. As orders may be removed and calculate their ratios dynamically, all issues fulfilling an order other than misconfiguration by the msg.sender are no-ops and DO NOT revert the transaction. This allows the msg.sender to optimistically provide a list of orders that they aren't sure will completely fill at a good price, and fallback to more reliable orders further down their list. Misconfiguration such as token mismatches are errors that revert as this is known and static at all times to the msg.sender so MUST be provided correctly. msg.sender MAY specify a minimum input that MUST be reached across all orders in the list, otherwise the transaction will revert, this MAY be set to zero. Exactly like withdraw, if there is an active flash loan for msg.sender they will have their outstanding loan reduced by the final input amount preferentially before sending any tokens. Notably this allows arb bots implemented as flash loan borrowers to connect orders against external liquidity directly by paying back the loan with a takeOrders call and outputting the result of the external trade. Rounding errors always favour the order never the msg.sender.

function takeOrders(TakeOrdersConfig calldata config) external returns (uint256 totalInput, uint256 totalOutput);

Parameters

NameTypeDescription
configTakeOrdersConfigThe constraints and list of orders to take, orders are processed sequentially in order as provided, there is NO ATTEMPT onchain to predict/filter/sort these orders other than evaluating them as provided. Inputs and outputs are from the perspective of msg.sender except for values specified by the orders themselves which are the from the perspective of that order.

Returns

NameTypeDescription
totalInputuint256Total tokens sent to msg.sender, taken from order vaults processed.
totalOutputuint256Total tokens taken from msg.sender and distributed between vaults.

clear

Allows msg.sender to match two live orders placed earlier by non-interactive parties and claim a bounty in the process. The clearer is free to select any two live orders on the order book for matching and as long as they have compatible tokens, ratios and amounts, the orders will clear. Clearing the orders DOES NOT remove them from the orderbook, they remain live until explicitly removed by their owner. Even if the input vault balances are completely emptied, the orders remain live until removed. This allows order owners to deploy a strategy over a long period of time and periodically top up the input vaults. Clearing two orders from the same owner is disallowed. Any mismatch in the ratios between the two orders will cause either more inputs than there are available outputs (transaction will revert) or less inputs than there are available outputs. In the latter case the excess outputs are given to the msg.sender of clear, to the vaults they specify in the clear config. This not only incentivises "automatic" clear calls for both alice and bob, but incentivises prioritising greater ratio differences with a larger bounty. The second point is important because it implicitly prioritises orders that are further from the current market price, thus putting constant increasing pressure on the entire system the further it drifts from the norm, no matter how esoteric the individual order expressions and sizings might be. All else equal there are several factors that would impact how reliably some order clears relative to the wider market, such as:

  • Bounties are effectively percentages of cleared amounts so larger orders have larger bounties and cover gas costs more easily
  • High gas on the network means that orders are harder to clear profitably so the negative spread of the ratios will need to be larger
  • Complex and stateful expressions cost more gas to evalulate so the negative spread will need to be larger
  • Erratic behavior of the order owner could reduce the willingness of third parties to interact if it could result in wasted gas due to orders suddently being removed before clearance etc.
  • Dynamic and highly volatile words used in the expression could be ignored or low priority by clearers who want to be sure that they can accurately predict the ratios that they include in their clearance
  • Geopolitical issues such as sanctions and regulatory restrictions could cause issues for certain owners and clearers
function clear(
    Order memory alice,
    Order memory bob,
    ClearConfig calldata clearConfig,
    SignedContext[] memory aliceSignedContext,
    SignedContext[] memory bobSignedContext
) external;

Parameters

NameTypeDescription
aliceOrderSome order to clear.
bobOrderAnother order to clear.
clearConfigClearConfigAdditional configuration for the clearance such as how to handle the bounty payment for the msg.sender.
aliceSignedContextSignedContext[]Optional signed context that is relevant to A.
bobSignedContextSignedContext[]Optional signed context that is relevant to B.

Events

Deposit

Some tokens have been deposited to a vault.

event Deposit(address sender, DepositConfig config);

Parameters

NameTypeDescription
senderaddressmsg.sender depositing tokens. Delegated deposits are NOT supported.
configDepositConfigAll config sent to the deposit call.

Withdraw

Some tokens have been withdrawn from a vault.

event Withdraw(address sender, WithdrawConfig config, uint256 amount);

Parameters

NameTypeDescription
senderaddressmsg.sender withdrawing tokens. Delegated withdrawals are NOT supported.
configWithdrawConfigAll config sent to the withdraw call.
amountuint256The amount of tokens withdrawn, can be less than the config amount if the vault does not have the funds available to cover the config amount. For example an active order might move tokens before the withdraw completes.

AddOrder

An order has been added to the orderbook. The order is permanently and always active according to its expression until/unless it is removed.

event AddOrder(address sender, IExpressionDeployerV3 expressionDeployer, Order order, uint256 orderHash);

Parameters

NameTypeDescription
senderaddressmsg.sender adding the order and is owner of the order.
expressionDeployerIExpressionDeployerV3The expression deployer that ran the integrity check for this order. This is NOT included in the Order itself but is important for offchain processes to ignore untrusted deployers before interacting with them.
orderOrderThe newly added order. MUST be handed back as-is when clearing orders and contains derived information in addition to the order config that was provided by the order owner.
orderHashuint256The hash of the order as it is recorded onchain. Only the hash is stored in Orderbook storage to avoid paying gas to store the entire order.

RemoveOrder

An order has been removed from the orderbook. This effectively deactivates it. Orders can be added again after removal.

event RemoveOrder(address sender, Order order, uint256 orderHash);

Parameters

NameTypeDescription
senderaddressmsg.sender removing the order and is owner of the order.
orderOrderThe removed order.
orderHashuint256The hash of the removed order.

TakeOrder

Some order has been taken by msg.sender. This is the same as them placing inverse orders then immediately clearing them all, but costs less gas and is more convenient and reliable. Analogous to a market buy against the specified orders. Each order that is matched within a the takeOrders loop emits its own individual event.

event TakeOrder(address sender, TakeOrderConfig config, uint256 input, uint256 output);

Parameters

NameTypeDescription
senderaddressmsg.sender taking the orders.
configTakeOrderConfigAll config defining the orders to attempt to take.
inputuint256The input amount from the perspective of sender.
outputuint256The output amount from the perspective of sender.

OrderNotFound

Emitted when attempting to match an order that either never existed or was removed. An event rather than an error so that we allow attempting many orders in a loop and NOT rollback on "best effort" basis to clear.

event OrderNotFound(address sender, address owner, uint256 orderHash);

Parameters

NameTypeDescription
senderaddressmsg.sender clearing the order that wasn't found.
owneraddressOwner of the order that was not found.
orderHashuint256Hash of the order that was not found.

OrderZeroAmount

Emitted when an order evaluates to a zero amount. An event rather than an error so that we allow attempting many orders in a loop and NOT rollback on a "best effort" basis to clear.

event OrderZeroAmount(address sender, address owner, uint256 orderHash);

Parameters

NameTypeDescription
senderaddressmsg.sender clearing the order that had a 0 amount.
owneraddressOwner of the order that evaluated to a 0 amount.
orderHashuint256Hash of the order that evaluated to a 0 amount.

OrderExceedsMaxRatio

Emitted when an order evaluates to a ratio exceeding the counterparty's maximum limit. An error rather than an error so that we allow attempting many orders in a loop and NOT rollback on a "best effort" basis to clear.

event OrderExceedsMaxRatio(address sender, address owner, uint256 orderHash);

Parameters

NameTypeDescription
senderaddressmsg.sender clearing the order that had an excess ratio.
owneraddressOwner of the order that had an excess ratio.
orderHashuint256Hash of the order that had an excess ratio.

Clear

Emitted before two orders clear. Covers both orders and includes all the state before anything is calculated.

event Clear(address sender, Order alice, Order bob, ClearConfig clearConfig);

Parameters

NameTypeDescription
senderaddressmsg.sender clearing both orders.
aliceOrderOne of the orders.
bobOrderThe other order.
clearConfigClearConfigAdditional config required to process the clearance.

AfterClear

Emitted after two orders clear. Includes all final state changes in the vault balances, including the clearer's vaults.

event AfterClear(address sender, ClearStateChange clearStateChange);

Parameters

NameTypeDescription
senderaddressmsg.sender clearing the order.
clearStateChangeClearStateChangeThe final vault state changes from the clearance.