IOrderBookV2
Inherits: IERC3156FlashLender, IInterpreterCallerV2
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
| Name | Type | Description |
|---|---|---|
owner | address | The owner of the vault. |
token | address | The token the vault is for. |
id | uint256 | The vault ID to read. |
Returns
| Name | Type | Description |
|---|---|---|
balance | uint256 | The 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
| Name | Type | Description |
|---|---|---|
config | DepositConfig | All 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
| Name | Type | Description |
|---|---|---|
config | WithdrawConfig | All 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
| Name | Type | Description |
|---|---|---|
config | OrderConfig | All 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
| Name | Type | Description |
|---|---|---|
order | Order | The 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
| Name | Type | Description |
|---|---|---|
config | TakeOrdersConfig | The 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
| Name | Type | Description |
|---|---|---|
totalInput | uint256 | Total tokens sent to msg.sender, taken from order vaults processed. |
totalOutput | uint256 | Total 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,
SignedContextV1[] memory aliceSignedContext,
SignedContextV1[] memory bobSignedContext
) external;
Parameters
| Name | Type | Description |
|---|---|---|
alice | Order | Some order to clear. |
bob | Order | Another order to clear. |
clearConfig | ClearConfig | Additional configuration for the clearance such as how to handle the bounty payment for the msg.sender. |
aliceSignedContext | SignedContextV1[] | Optional signed context that is relevant to A. |
bobSignedContext | SignedContextV1[] | 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
| Name | Type | Description |
|---|---|---|
sender | address | msg.sender depositing tokens. Delegated deposits are NOT supported. |
config | DepositConfig | All config sent to the deposit call. |
Withdraw
Some tokens have been withdrawn from a vault.
event Withdraw(address sender, WithdrawConfig config, uint256 amount);
Parameters
| Name | Type | Description |
|---|---|---|
sender | address | msg.sender withdrawing tokens. Delegated withdrawals are NOT supported. |
config | WithdrawConfig | All config sent to the withdraw call. |
amount | uint256 | The 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, IExpressionDeployerV2 expressionDeployer, Order order, uint256 orderHash);
Parameters
| Name | Type | Description |
|---|---|---|
sender | address | msg.sender adding the order and is owner of the order. |
expressionDeployer | IExpressionDeployerV2 | The 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. |
order | Order | The 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. |
orderHash | uint256 | The 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
| Name | Type | Description |
|---|---|---|
sender | address | msg.sender removing the order and is owner of the order. |
order | Order | The removed order. |
orderHash | uint256 | The 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
| Name | Type | Description |
|---|---|---|
sender | address | msg.sender taking the orders. |
config | TakeOrderConfig | All config defining the orders to attempt to take. |
input | uint256 | The input amount from the perspective of sender. |
output | uint256 | The 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
| Name | Type | Description |
|---|---|---|
sender | address | msg.sender clearing the order that wasn't found. |
owner | address | Owner of the order that was not found. |
orderHash | uint256 | Hash 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
| Name | Type | Description |
|---|---|---|
sender | address | msg.sender clearing the order that had a 0 amount. |
owner | address | Owner of the order that evaluated to a 0 amount. |
orderHash | uint256 | Hash 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
| Name | Type | Description |
|---|---|---|
sender | address | msg.sender clearing the order that had an excess ratio. |
owner | address | Owner of the order that had an excess ratio. |
orderHash | uint256 | Hash 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
| Name | Type | Description |
|---|---|---|
sender | address | msg.sender clearing both orders. |
alice | Order | One of the orders. |
bob | Order | The other order. |
clearConfig | ClearConfig | Additional 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
| Name | Type | Description |
|---|---|---|
sender | address | msg.sender clearing the order. |
clearStateChange | ClearStateChange | The final vault state changes from the clearance. |