rain.orderbook
Docs at https://rainprotocol.github.io/foundry.template
Use as template
forge init -t rainprotocol/foundry.template <projectname>
cd <projectname>
forge install foundry-rs/forge-std
Then update the readme, set the docs url and configure github pages on github repo settings.
For CI deployments, setup all the environment variables and define contracts to deploy in the matrix.
Contents
- MinimumOutput
- Initializing
- NonZeroBeforeArbStack
- BadLender
- NonZeroBeforeArbInputs
- OrderBookV3ArbOrderTakerConfigV1
- OrderBookV3ArbOrderTaker
- OrderBookV3ArbOrderTaker constants
- BadInitiator
- FlashLoanFailed
- SwapFailed
- NonZeroBeforeArbInputs
- OrderBookV3FlashBorrowerConfigV2
- OrderBookV3FlashBorrower
- OrderBookV3FlashBorrower constants
- FlashLenderCallbackFailed
- OrderBookV3FlashLender
- OrderBookV3FlashLender constants
MinimumOutput
Thrown when the minimum output for the sender is not met after the arb.
error MinimumOutput(uint256 minimum, uint256 actual);
Parameters
| Name | Type | Description |
|---|---|---|
minimum | uint256 | The minimum output expected by the sender. |
actual | uint256 | The actual output that would be received by the sender. |
Initializing
Thrown when calling functions while the contract is still initializing.
error Initializing();
NonZeroBeforeArbStack
Thrown when the stack is not empty after the access control dispatch.
error NonZeroBeforeArbStack();
BadLender
Thrown when the lender is not the trusted OrderBook.
error BadLender(address badLender);
Parameters
| Name | Type | Description |
|---|---|---|
badLender | address | The untrusted lender calling onFlashLoan. |
NonZeroBeforeArbInputs
Thrown when "before arb" wants inputs that we don't have.
error NonZeroBeforeArbInputs(uint256 inputs);
OrderBookV3ArbOrderTakerConfigV1
Config for OrderBookV3ArbOrderTakerConfigV1 to initialize.
struct OrderBookV3ArbOrderTakerConfigV1 {
address orderBook;
EvaluableConfigV3 evaluableConfig;
bytes implementationData;
}
Properties
| Name | Type | Description |
|---|---|---|
orderBook | address | The IOrderBookV3 to use for takeOrders. |
evaluableConfig | EvaluableConfigV3 | The config to eval for access control to arb. |
implementationData | bytes | Arbitrary bytes to pass to the implementation in the beforeInitialize hook. |
OrderBookV3ArbOrderTaker
Inherits: IOrderBookV3ArbOrderTaker, ReentrancyGuard, Initializable, ICloneableV2, DeployerDiscoverableMetaV3, ERC165
State Variables
sOrderBook
IOrderBookV3 public sOrderBook;
sI9rDispatch
EncodedDispatch public sI9rDispatch;
sI9r
IInterpreterV2 public sI9r;
sI9rStore
IInterpreterStoreV1 public sI9rStore;
Functions
constructor
constructor(bytes32 metaHash, DeployerDiscoverableMetaV3ConstructionConfig memory config)
DeployerDiscoverableMetaV3(metaHash, config);
supportsInterface
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool);
_beforeInitialize
function _beforeInitialize(bytes memory data) internal virtual;
onlyNotInitializing
Ensure the contract is not initializing.
modifier onlyNotInitializing();
initialize
function initialize(OrderBookV3ArbOrderTakerConfigV1 calldata) external pure returns (bytes32);
initialize
function initialize(bytes memory data) external initializer nonReentrant returns (bytes32);
arb
function arb(TakeOrdersConfigV2 calldata takeOrders, uint256 minimumSenderOutput)
external
payable
nonReentrant
onlyNotInitializing;
onTakeOrders
Called by OrderBookV3 when takeOrders is called with non-zero
data, if it caused a non-zero input amount. I.e. if the order(s) taker
received some tokens. Input and output directions are relative to the
IOrderBookV3OrderTaker contract. If the order(s) taker had an active
debt from a flash loan then that debt will be paid before calculating
any input amounts sent.
i.e. the debt is deducted from the input amount before this callback is
called.
function onTakeOrders(address, address, uint256, uint256, bytes calldata) public virtual override onlyNotInitializing;
Parameters
| Name | Type | Description |
|---|---|---|
<none> | address | |
<none> | address | |
<none> | uint256 | |
<none> | uint256 | |
<none> | bytes |
Events
Initialize
event Initialize(address sender, OrderBookV3ArbOrderTakerConfigV1 config);
Constants
BEFORE_ARB_SOURCE_INDEX
"Before arb" is evaluabled before the arb is executed. Ostensibly this is to allow for access control to the arb, the return values are ignored.
SourceIndexV2 constant BEFORE_ARB_SOURCE_INDEX = SourceIndexV2.wrap(0);
BEFORE_ARB_MIN_OUTPUTS
"Before arb" has no return values.
uint256 constant BEFORE_ARB_MIN_OUTPUTS = 0;
BEFORE_ARB_MAX_OUTPUTS
"Before arb" has no return values.
uint16 constant BEFORE_ARB_MAX_OUTPUTS = 0;
BadInitiator
Thrown when the initiator is not the order book.
error BadInitiator(address badInitiator);
Parameters
| Name | Type | Description |
|---|---|---|
badInitiator | address | The untrusted initiator of the flash loan. |
FlashLoanFailed
Thrown when the flash loan fails somehow.
error FlashLoanFailed();
SwapFailed
Thrown when the swap fails.
error SwapFailed();
NonZeroBeforeArbInputs
Thrown when "Before arb" expects inputs.
error NonZeroBeforeArbInputs();
OrderBookV3FlashBorrowerConfigV2
Config for OrderBookV3FlashBorrower to initialize.
struct OrderBookV3FlashBorrowerConfigV2 {
address orderBook;
EvaluableConfigV3 evaluableConfig;
bytes implementationData;
}
Properties
| Name | Type | Description |
|---|---|---|
orderBook | address | The IOrderBookV3 contract to arb against. |
evaluableConfig | EvaluableConfigV3 | The config to eval for access control to arb. |
implementationData | bytes | Arbitrary bytes to pass to the implementation in the beforeInitialize hook. |
OrderBookV3FlashBorrower
Inherits: IERC3156FlashBorrower, ICloneableV2, ReentrancyGuard, Initializable, DeployerDiscoverableMetaV3, ERC165
Abstract contract that liq-source specifialized contracts can inherit
to provide flash loan based arbitrage against external liquidity sources to
fill orderbook orders.
For example consider a simple order:
input = DAI
output = USDT
IORatio = 1.01e18
Order amount = 100e18
Assume external liq is offering 102 DAI per USDT so it exceeds the IO ratio
but the order itself has no way to interact with the external contract.
The OrderBookFlashBorrower can:
- Flash loan 100 USDT from
Orderbook - Sell the 100 USDT for 102 DAI on external liq
- Take the order, giving 101 DAI and paying down 100 USDT loan
- Keep 1 DAI profit
As this contract is expected to be cloned using a minimal proxy there will
be many copies of it in the wild. Each copy can be access gated by a Rain
expression that is evaluated before anything else happens in
arb. There are many reasons why this might be desirable: - Regulatory reasons that restrict how an arb bot operator can interact with orders and/or external liq.
- The arb operator wants to attempt to prevent front running by other bots.
- The arb operator may prefer a dedicated instance of the contract to make it easier to track profits, etc.
State Variables
sOrderBook
OrderBook contract to lend and arb against.
IOrderBookV3 public sOrderBook;
sI9rDispatch
The encoded dispatch that will run for access control to arb.
EncodedDispatch public sI9rDispatch;
sI9r
The interpreter that will eval access control to arb.
IInterpreterV2 public sI9r;
sI9rStore
The associated store for the interpreter.
IInterpreterStoreV1 public sI9rStore;
Functions
constructor
constructor(bytes32 metaHash, DeployerDiscoverableMetaV3ConstructionConfig memory config)
DeployerDiscoverableMetaV3(metaHash, config);
supportsInterface
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool);
_beforeInitialize
Hook called before initialize happens. Inheriting contracts can perform internal state maintenance before any external contract calls are made.
function _beforeInitialize(bytes memory data) internal virtual;
Parameters
| Name | Type | Description |
|---|---|---|
data | bytes | Arbitrary bytes the child may use to initialize. |
initialize
Type hints for the input encoding for the initialize function.
Reverts ALWAYS with InitializeSignatureFn as per ICloneableV2.
function initialize(OrderBookV3FlashBorrowerConfigV2 calldata) external pure returns (bytes32);
initialize
function initialize(bytes memory data) external initializer nonReentrant returns (bytes32);
onlyNotInitializing
Ensure the contract is not initializing.
modifier onlyNotInitializing();
_exchange
Hook that inheriting contracts MUST implement in order to achieve
anything other than raising the ambient temperature of the room.
_exchange is responsible for converting the flash loaned assets into
the assets required to fill the orders. Generally this can only be
achieved by interacting with an external liquidity source that is
offering a better price than the orders require.
function _exchange(TakeOrdersConfigV2 memory takeOrders, bytes memory exchangeData) internal virtual;
Parameters
| Name | Type | Description |
|---|---|---|
takeOrders | TakeOrdersConfigV2 | As per arb. |
exchangeData | bytes | As per arb. |
onFlashLoan
Receive a flash loan.
function onFlashLoan(address initiator, address, uint256, uint256, bytes calldata data)
external
onlyNotInitializing
returns (bytes32);
Parameters
| Name | Type | Description |
|---|---|---|
initiator | address | The initiator of the loan. |
<none> | address | |
<none> | uint256 | |
<none> | uint256 | |
data | bytes | Arbitrary data structure, intended to contain user-defined parameters. |
Returns
| Name | Type | Description |
|---|---|---|
<none> | bytes32 | The keccak256 hash of "ERC3156FlashBorrower.onFlashLoan" |
arb
Primary function to process arbitrage opportunities.
Firstly the access gate is evaluated to ensure the sender is allowed to
submit arbitrage. If there is no access control the sender should expect
to be front run on the arb for any sufficiently profitable opportunity.
This may be desirable in some cases, as the sender may simply want to
be clearing the orderbook and they are expecting profit/utility from the
orderbook strategies themselves somehow.
Secondly the flash loan is taken and the _exchange hook is called to
allow the inheriting contract to convert the flash loaned assets into
the assets required to fill the orders.
Finally the orders are taken and the remaining assets are sent to the
sender.
function arb(TakeOrdersConfigV2 calldata takeOrders, uint256 minimumSenderOutput, bytes calldata exchangeData)
external
payable
nonReentrant
onlyNotInitializing;
Parameters
| Name | Type | Description |
|---|---|---|
takeOrders | TakeOrdersConfigV2 | As per IOrderBookV3.takeOrders. |
minimumSenderOutput | uint256 | The minimum output that must be sent to the sender by the end of the arb call. This, in combination with the orderbook's own asset handling, is expected to REPLACE the standard slippage protection that would be provided by a DEX. The sender is expected to calculate absolute values based on prevailing conditions such as gas price and the risk of holding the assets any arb profit is denominated in. |
exchangeData | bytes | Arbitrary bytes that will be passed to _exchange after the flash loan is taken. The inheriting contract is responsible for decoding this data and defining how it controls interactions with the external liquidity. For example, GenericPoolOrderBookV3FlashBorrower uses this data as a literal encoded external call. |
Events
Initialize
Emitted when the contract is initialized. Contains the OrderBookFlashBorrowerConfig struct to ensure the type appears in the ABI.
event Initialize(address sender, OrderBookV3FlashBorrowerConfigV2 config);
Constants
BEFORE_ARB_SOURCE_INDEX
"Before arb" is evaluated before the flash loan is taken. Ostensibly allows for some kind of access control to the arb.
SourceIndexV2 constant BEFORE_ARB_SOURCE_INDEX = SourceIndexV2.wrap(0);
BEFORE_ARB_MIN_INPUTS
"Before arb" has no inputs.
uint256 constant BEFORE_ARB_MIN_INPUTS = 0;
BEFORE_ARB_MIN_OUTPUTS
"Before arb" has no outputs.
uint256 constant BEFORE_ARB_MIN_OUTPUTS = 0;
BEFORE_ARB_MAX_OUTPUTS
"Before arb" has no outputs.
uint256 constant BEFORE_ARB_MAX_OUTPUTS = 0;
FlashLenderCallbackFailed
Thrown when the onFlashLoan callback returns anything other than
ON_FLASH_LOAN_CALLBACK_SUCCESS.
error FlashLenderCallbackFailed(bytes32 result);
Parameters
| Name | Type | Description |
|---|---|---|
result | bytes32 | The value that was returned by onFlashLoan. |
OrderBookV3FlashLender
Inherits: IERC3156FlashLender
Implements IERC3156FlashLender for OrderBook. Based on the
reference implementation by Alberto Cuesta Cañada found at
https://eips.ethereum.org/EIPS/eip-3156#flash-loan-reference-implementation
Functions
flashLoan
Initiate a flash loan.
function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data)
external
override
returns (bool);
Parameters
| Name | Type | Description |
|---|---|---|
receiver | IERC3156FlashBorrower | The receiver of the tokens in the loan, and the receiver of the callback. |
token | address | The loan currency. |
amount | uint256 | The amount of tokens lent. |
data | bytes | Arbitrary data structure, intended to contain user-defined parameters. |
flashFee
The fee to be charged for a given loan.
function flashFee(address, uint256) external pure override returns (uint256);
Parameters
| Name | Type | Description |
|---|---|---|
<none> | address | |
<none> | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
<none> | uint256 | The amount of token to be charged for the loan, on top of the returned principal. |
maxFlashLoan
There's no limit to the size of a flash loan from Orderbook other than
the current tokens deposited in Orderbook. If there is an active debt
then loans are disabled so the max becomes 0 until after repayment.
The amount of currency available to be lent.
function maxFlashLoan(address token) external view override returns (uint256);
Parameters
| Name | Type | Description |
|---|---|---|
token | address | The loan currency. |
Returns
| Name | Type | Description |
|---|---|---|
<none> | uint256 | The amount of token that can be borrowed. |
Constants
FLASH_FEE
Flash fee is always 0 for orderbook as there's no entity to take
revenue for Orderbook and its more important anyway that flashloans happen
to connect external liquidity to live orders via arbitrage.
uint256 constant FLASH_FEE = 0;
Contents
- GenericPoolOrderBookV3ArbOrderTaker
- GenericPoolOrderBookV3ArbOrderTaker constants
- GenericPoolOrderBookV3FlashBorrower
- GenericPoolOrderBookV3FlashBorrower constants
- ReentrancyGuardReentrantCall
- NotOrderOwner
- TokenMismatch
- TokenDecimalsMismatch
- MinimumInput
- SameOwner
- UnsupportedCalculateInputs
- UnsupportedCalculateOutputs
- UnsupportedHandleInputs
- OrderIOCalculationV2
- Output18Amount
- Input18Amount
- OrderBook
- OrderBook constants
- RouteProcessorOrderBookV3ArbOrderTaker
- RouteProcessorOrderBookV3ArbOrderTaker constants
GenericPoolOrderBookV3ArbOrderTaker
Inherits: OrderBookV3ArbOrderTaker
Functions
constructor
constructor(DeployerDiscoverableMetaV3ConstructionConfig memory config)
OrderBookV3ArbOrderTaker(CALLER_META_HASH, config);
onTakeOrders
function onTakeOrders(
address inputToken,
address outputToken,
uint256 inputAmountSent,
uint256 totalOutputAmount,
bytes calldata takeOrdersData
) public virtual override;
fallback
Allow receiving gas.
fallback() external onlyNotInitializing;
Constants
CALLER_META_HASH
bytes32 constant CALLER_META_HASH = bytes32(0xe1d075e6f17f6706d942759ec359deb7f354ab4ac55e58eda2870c0ab3a89fa5);
GenericPoolOrderBookV3FlashBorrower
Inherits: OrderBookV3FlashBorrower
Functions
constructor
constructor(DeployerDiscoverableMetaV3ConstructionConfig memory config)
OrderBookV3FlashBorrower(CALLER_META_HASH, config);
_exchange
Hook that inheriting contracts MUST implement in order to achieve
anything other than raising the ambient temperature of the room.
_exchange is responsible for converting the flash loaned assets into
the assets required to fill the orders. Generally this can only be
achieved by interacting with an external liquidity source that is
offering a better price than the orders require.
function _exchange(TakeOrdersConfigV2 memory takeOrders, bytes memory exchangeData) internal virtual override;
Parameters
| Name | Type | Description |
|---|---|---|
takeOrders | TakeOrdersConfigV2 | As per arb. |
exchangeData | bytes | As per arb. |
fallback
Allow receiving gas.
fallback() external onlyNotInitializing;
Constants
CALLER_META_HASH
*Metadata hash for DeployerDiscoverableMetaV1.
- ABI for GenericPoolOrderBookV3FlashBorrower
- Interpreter caller metadata V1 for GenericPoolOrderBookV3FlashBorrower*
bytes32 constant CALLER_META_HASH = bytes32(0x52cdec296c4ecb0b2452c9e314df50e1b5d52fef03927934b7426ff6f150f0fe);
ReentrancyGuardReentrantCall
This will exist in a future version of Open Zeppelin if their main branch is to be believed.
error ReentrancyGuardReentrantCall();
NotOrderOwner
Thrown when the msg.sender modifying an order is not its owner.
error NotOrderOwner(address sender, address owner);
Parameters
| Name | Type | Description |
|---|---|---|
sender | address | msg.sender attempting to modify the order. |
owner | address | The owner of the order. |
TokenMismatch
Thrown when the input and output tokens don't match, in either direction.
error TokenMismatch(address aliceToken, address bobToken);
Parameters
| Name | Type | Description |
|---|---|---|
aliceToken | address | The input or output of one order. |
bobToken | address | The input or output of the other order that doesn't match a. |
TokenDecimalsMismatch
Thrown when the input and output token decimals don't match, in either direction.
error TokenDecimalsMismatch(uint8 aliceTokenDecimals, uint8 bobTokenDecimals);
Parameters
| Name | Type | Description |
|---|---|---|
aliceTokenDecimals | uint8 | The input or output decimals of one order. |
bobTokenDecimals | uint8 | The input or output decimals of the other order. |
MinimumInput
Thrown when the minimum input is not met.
error MinimumInput(uint256 minimumInput, uint256 input);
Parameters
| Name | Type | Description |
|---|---|---|
minimumInput | uint256 | The minimum input required. |
input | uint256 | The input that was achieved. |
SameOwner
Thrown when two orders have the same owner during clear.
error SameOwner(address owner);
Parameters
| Name | Type | Description |
|---|---|---|
owner | address | The owner of both orders. |
UnsupportedCalculateInputs
Thrown when calculate order expression wants inputs.
error UnsupportedCalculateInputs(uint256 inputs);
Parameters
| Name | Type | Description |
|---|---|---|
inputs | uint256 | The inputs the expression wants. |
UnsupportedCalculateOutputs
Thrown when calculate order expression offers too few outputs.
error UnsupportedCalculateOutputs(uint256 outputs);
Parameters
| Name | Type | Description |
|---|---|---|
outputs | uint256 | The outputs the expression offers. |
UnsupportedHandleInputs
Thrown when handle IO expression wants inputs.
error UnsupportedHandleInputs(uint256 inputs);
Parameters
| Name | Type | Description |
|---|---|---|
inputs | uint256 | The inputs the expression wants. |
OrderIOCalculationV2
All information resulting from an order calculation that allows for vault IO to be calculated and applied, then the handle IO entrypoint to be dispatched.
struct OrderIOCalculationV2 {
OrderV2 order;
uint256 outputIOIndex;
Output18Amount outputMax;
uint256 IORatio;
uint256[][] context;
StateNamespace namespace;
uint256[] kvs;
}
Properties
| Name | Type | Description |
|---|---|---|
order | OrderV2 | |
outputIOIndex | uint256 | |
outputMax | Output18Amount | The UNSCALED maximum output calculated by the order expression. WILL BE RESCALED ACCORDING TO TOKEN DECIMALS to an 18 fixed point decimal number for the purpose of calculating actual vault movements. The output max is CAPPED AT THE OUTPUT VAULT BALANCE OF THE ORDER OWNER. The order is guaranteed that the total output of this single clearance cannot exceed this (subject to rescaling). It is up to the order expression to track values over time if the output max is to impose a global limit across many transactions and counterparties. |
IORatio | uint256 | The UNSCALED order ratio as input/output from the perspective of the order. As each counterparty's input is the other's output, the IORatio calculated by each order is inverse of its counterparty. IORatio is SCALED ACCORDING TO TOKEN DECIMALS to allow 18 decimal fixed point math over the vault balances. I.e. 1e18 returned from the expression is ALWAYS "one" as ECONOMIC EQUIVALENCE between two tokens, but this will be rescaled according to the decimals of the token. For example, if DAI and USDT have a ratio of 1e18 then in reality 1e12 DAI will move in the vault for every 1 USDT that moves, because DAI has 1e18 decimals per $1 peg and USDT has 1e6 decimals per $1 peg. THE ORDER DEFINES THE DECIMALS for each token, NOT the token itself, because the token MAY NOT report its decimals as per it being optional in the ERC20 specification. |
context | uint256[][] | The entire 2D context array, initialized from the context passed into the order calculations and then populated with the order calculations and vault IO before being passed back to handle IO entrypoint. |
namespace | StateNamespace | The StateNamespace to be passed to the store for calculate IO state changes. |
kvs | uint256[] | KVs returned from calculate order entrypoint to pass to the store before calling handle IO entrypoint. |
Output18Amount
type Output18Amount is uint256;
Input18Amount
type Input18Amount is uint256;
OrderBook
Inherits: IOrderBookV3, ReentrancyGuard, Multicall, OrderBookV3FlashLender, DeployerDiscoverableMetaV3
State Variables
sOrders
All hashes of all active orders. There's nothing interesting in the value it's just nonzero if the order is live. The key is the hash of the order. Removing an order sets the value back to zero so it is identical to the order never existing. The order hash includes its owner so there's no need to build a multi level mapping, each order hash MUST uniquely identify the order globally. order hash => order is live
mapping(bytes32 orderHash => uint256 liveness) internal sOrders;
sVaultBalances
Vault balances are stored in a mapping of owner => token => vault ID
This gives 1:1 parity with the IOrderBookV1 interface but keeping the
sFoo naming convention for storage variables.
mapping(address owner => mapping(address token => mapping(uint256 vaultId => uint256 balance))) internal sVaultBalances;
Functions
constructor
Initializes the orderbook upon construction for compatibility with Open Zeppelin upgradeable contracts. Orderbook itself does NOT support factory deployments as each order is a unique expression deployment rather than needing to wrap up expressions with proxies.
constructor(DeployerDiscoverableMetaV3ConstructionConfig memory config)
DeployerDiscoverableMetaV3(CALLER_META_HASH, config);
vaultBalance
Get the current balance of a vault for a given owner, token and vault ID.
function vaultBalance(address owner, address token, uint256 vaultId) external view override returns (uint256);
Parameters
| Name | Type | Description |
|---|---|---|
owner | address | The owner of the vault. |
token | address | The token the vault is for. |
vaultId | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
<none> | uint256 | balance The current balance of the vault. |
orderExists
Returns true if the order exists, false otherwise.
function orderExists(bytes32 orderHash) external view override returns (bool);
Parameters
| Name | Type | Description |
|---|---|---|
orderHash | bytes32 | The hash of the order to check. |
Returns
| Name | Type | Description |
|---|---|---|
<none> | bool | exists True if the order exists, false otherwise. |
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.
Vault IDs are namespaced by the token address so there is no risk of
collision between tokens. For example, vault ID 0 for token A is
completely different to vault ID 0 for token B.
0 amount deposits are unsupported as underlying token contracts
handle 0 value transfers differently and this would be a source of
confusion. The order book MUST revert with ZeroDepositAmount if the
amount is zero.
function deposit(address token, uint256 vaultId, uint256 amount) external nonReentrant;
Parameters
| Name | Type | Description |
|---|---|---|
token | address | The token to deposit. |
vaultId | uint256 | The vault ID to deposit under. |
amount | uint256 | The amount of tokens to 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. MUST revert if the amount requested to withdraw is zero. The withdrawal MAY still not move any tokens (without revert) if the vault balance is zero, or the withdrawal is used to repay a flash loan, or due to any other internal accounting.
function withdraw(address token, uint256 vaultId, uint256 targetAmount) external nonReentrant;
Parameters
| Name | Type | Description |
|---|---|---|
token | address | The token to withdraw. |
vaultId | uint256 | The vault ID to withdraw from. |
targetAmount | uint256 | The amount of tokens to attempt to withdraw. MAY result in fewer tokens withdrawn if the vault balance is lower than the target amount. MAY NOT be zero, the order book MUST revert with ZeroWithdrawTargetAmount if the amount is zero. |
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.
MUST revert with OrderNoSources if the order has no associated
calculation and OrderNoHandleIO if the order has no handle IO
entrypoint. The calculation MUST return at least two values from
evaluation, the maximum amount and the IO ratio. The handle IO entrypoint
SHOULD return zero values from evaluation. Either MAY revert during
evaluation on the interpreter, which MUST prevent the order from
clearing.
MUST revert with OrderNoInputs if the order has no inputs.
MUST revert with OrderNoOutputs if the order has no outputs.
If the order already exists, the order book MUST NOT change state, which
includes not emitting an event. Instead it MUST return false. If the
order book modifies state it MUST emit an AddOrder event and return
true.
function addOrder(OrderConfigV2 calldata config) external nonReentrant returns (bool stateChanged);
Parameters
| Name | Type | Description |
|---|---|---|
config | OrderConfigV2 | All config required to build an Order. |
Returns
| Name | Type | Description |
|---|---|---|
stateChanged | bool | True if the order was added, false if it already existed. |
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(OrderV2 calldata order) external nonReentrant returns (bool stateChanged);
Parameters
| Name | Type | Description |
|---|---|---|
order | OrderV2 | The Order data exactly as it was added. |
Returns
| Name | Type | Description |
|---|---|---|
stateChanged | bool | True if the order was removed, false if it did not exist. |
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(TakeOrdersConfigV2 calldata config)
external
nonReentrant
returns (uint256 totalTakerInput, uint256 totalTakerOutput);
Parameters
| Name | Type | Description |
|---|---|---|
config | TakeOrdersConfigV2 | 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 |
|---|---|---|
totalTakerInput | uint256 | totalInput Total tokens sent to msg.sender, taken from order vaults processed. |
totalTakerOutput | uint256 | totalOutput 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(
OrderV2 memory aliceOrder,
OrderV2 memory bobOrder,
ClearConfig calldata clearConfig,
SignedContextV1[] memory aliceSignedContext,
SignedContextV1[] memory bobSignedContext
) external nonReentrant;
Parameters
| Name | Type | Description |
|---|---|---|
aliceOrder | OrderV2 | |
bobOrder | OrderV2 | |
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. |
calculateOrderIO
Main entrypoint into an order calculates the amount and IO ratio. Both are always treated as 18 decimal fixed point values and then rescaled according to the order's definition of each token's actual fixed point decimals.
function calculateOrderIO(
OrderV2 memory order,
uint256 inputIOIndex,
uint256 outputIOIndex,
address counterparty,
SignedContextV1[] memory signedContext
) internal view returns (OrderIOCalculationV2 memory);
Parameters
| Name | Type | Description |
|---|---|---|
order | OrderV2 | The order to evaluate. |
inputIOIndex | uint256 | The index of the input token being calculated for. |
outputIOIndex | uint256 | The index of the output token being calculated for. |
counterparty | address | The counterparty of the order as it is currently being cleared against. |
signedContext | SignedContextV1[] | Any signed context provided by the clearer/taker that the order may need for its calculations. |
recordVaultIO
Given an order, final input and output amounts and the IO calculation
verbatim from _calculateOrderIO, dispatch the handle IO entrypoint if
it exists and update the order owner's vault balances.
function recordVaultIO(uint256 input, uint256 output, OrderIOCalculationV2 memory orderIOCalculation) internal;
Parameters
| Name | Type | Description |
|---|---|---|
input | uint256 | The exact token input amount to move into the owner's vault. |
output | uint256 | The exact token output amount to move out of the owner's vault. |
orderIOCalculation | OrderIOCalculationV2 | The verbatim order IO calculation returned by _calculateOrderIO. |
handleIO
function handleIO(OrderIOCalculationV2 memory orderIOCalculation) internal;
calculateClearStateChange
Calculates the clear state change given both order calculations for order alice and order bob. The input of each is their output multiplied by their IO ratio and the output of each is the smaller of their maximum output and the counterparty IO * max output.
function calculateClearStateChange(
OrderIOCalculationV2 memory aliceOrderIOCalculation,
OrderIOCalculationV2 memory bobOrderIOCalculation
) internal pure returns (ClearStateChange memory clearStateChange);
Parameters
| Name | Type | Description |
|---|---|---|
aliceOrderIOCalculation | OrderIOCalculationV2 | Order calculation for Alice. |
bobOrderIOCalculation | OrderIOCalculationV2 | Order calculation for Bob. |
Returns
| Name | Type | Description |
|---|---|---|
clearStateChange | ClearStateChange | The clear state change with absolute inputs and outputs for Alice and Bob. |
calculateClearStateAlice
function calculateClearStateAlice(
OrderIOCalculationV2 memory aliceOrderIOCalculation,
OrderIOCalculationV2 memory bobOrderIOCalculation
) internal pure returns (uint256 aliceInput, uint256 aliceOutput);
_calculateOrderDispatch
function _calculateOrderDispatch(address expression_) internal pure returns (EncodedDispatch);
_handleIODispatch
function _handleIODispatch(address expression_) internal pure returns (EncodedDispatch);
Constants
ORDER_LIVE
Stored value for a live order. NOT a boolean because storing a boolean is more expensive than storing a uint256.
uint256 constant ORDER_LIVE = 1;
ORDER_DEAD
Stored value for a dead order. 0 is chosen because it is the default
value for a mapping, which means all orders are dead unless explicitly made
live.
uint256 constant ORDER_DEAD = 0;
CALCULATE_ORDER_ENTRYPOINT
Entrypoint to a calculate the amount and ratio of an order.
SourceIndexV2 constant CALCULATE_ORDER_ENTRYPOINT = SourceIndexV2.wrap(0);
HANDLE_IO_ENTRYPOINT
Entrypoint to handle the final internal vault movements resulting from matching multiple calculated orders.
SourceIndexV2 constant HANDLE_IO_ENTRYPOINT = SourceIndexV2.wrap(1);
CALCULATE_ORDER_MIN_OUTPUTS
Minimum outputs for calculate order are the amount and ratio.
uint256 constant CALCULATE_ORDER_MIN_OUTPUTS = 2;
CALCULATE_ORDER_MAX_OUTPUTS
Maximum outputs for calculate order are the amount and ratio.
uint16 constant CALCULATE_ORDER_MAX_OUTPUTS = 2;
HANDLE_IO_MIN_OUTPUTS
Handle IO has no outputs as it only responds to vault movements.
uint256 constant HANDLE_IO_MIN_OUTPUTS = 0;
HANDLE_IO_MAX_OUTPUTS
Handle IO has no outputs as it only response to vault movements.
uint16 constant HANDLE_IO_MAX_OUTPUTS = 0;
CALLING_CONTEXT_COLUMNS
Orderbook context is actually fairly complex. The calling context column is populated before calculate order, but the remaining columns are only available to handle IO as they depend on the full evaluation of calculuate order, and cross referencing against the same from the counterparty, as well as accounting limits such as current vault balances, etc. The token address and decimals for vault inputs and outputs IS available to the calculate order entrypoint, but not the final vault balances/diff.
uint256 constant CALLING_CONTEXT_COLUMNS = 4;
CONTEXT_BASE_COLUMN
Base context from LibContext.
uint256 constant CONTEXT_BASE_COLUMN = 0;
CONTEXT_CALLING_CONTEXT_COLUMN
Contextual data available to both calculate order and handle IO. The order hash, order owner and order counterparty. IMPORTANT NOTE that the typical base context of an order with the caller will often be an unrelated clearer of the order rather than the owner or counterparty.
uint256 constant CONTEXT_CALLING_CONTEXT_COLUMN = 1;
CONTEXT_CALCULATIONS_COLUMN
Calculations column contains the DECIMAL RESCALED calculations but otherwise provided as-is according to calculate order entrypoint
uint256 constant CONTEXT_CALCULATIONS_COLUMN = 2;
CONTEXT_VAULT_INPUTS_COLUMN
Vault inputs are the literal token amounts and vault balances before and after for the input token from the perspective of the order. MAY be significantly different to the calculated amount due to insufficient vault balances from either the owner or counterparty, etc.
uint256 constant CONTEXT_VAULT_INPUTS_COLUMN = 3;
CONTEXT_VAULT_OUTPUTS_COLUMN
Vault outputs are the same as vault inputs but for the output token from the perspective of the order.
uint256 constant CONTEXT_VAULT_OUTPUTS_COLUMN = 4;
CONTEXT_VAULT_IO_TOKEN
Row of the token address for vault inputs and outputs columns.
uint256 constant CONTEXT_VAULT_IO_TOKEN = 0;
CONTEXT_VAULT_IO_TOKEN_DECIMALS
Row of the token decimals for vault inputs and outputs columns.
uint256 constant CONTEXT_VAULT_IO_TOKEN_DECIMALS = 1;
CONTEXT_VAULT_IO_VAULT_ID
Row of the vault ID for vault inputs and outputs columns.
uint256 constant CONTEXT_VAULT_IO_VAULT_ID = 2;
CONTEXT_VAULT_IO_BALANCE_BEFORE
Row of the vault balance before the order was cleared for vault inputs and outputs columns.
uint256 constant CONTEXT_VAULT_IO_BALANCE_BEFORE = 3;
CONTEXT_VAULT_IO_BALANCE_DIFF
Row of the vault balance difference after the order was cleared for
vault inputs and outputs columns. The diff is ALWAYS POSITIVE as it is a
uint256 so it must be added to input balances and subtraced from output
balances.
uint256 constant CONTEXT_VAULT_IO_BALANCE_DIFF = 4;
CONTEXT_VAULT_IO_ROWS
Length of a vault IO column.
uint256 constant CONTEXT_VAULT_IO_ROWS = 5;
CALLER_META_HASH
Hash of the caller contract metadata for construction.
bytes32 constant CALLER_META_HASH = bytes32(0x4383dd3c5a557f161038940161f880963e1d4a31e8f2da771427b891956ad831);
RouteProcessorOrderBookV3ArbOrderTaker
Inherits: OrderBookV3ArbOrderTaker
State Variables
sRouteProcessor
IRouteProcessor public sRouteProcessor;
Functions
constructor
constructor(DeployerDiscoverableMetaV3ConstructionConfig memory config)
OrderBookV3ArbOrderTaker(CALLER_META_HASH, config);
_beforeInitialize
function _beforeInitialize(bytes memory data) internal virtual override;
onTakeOrders
function onTakeOrders(
address inputToken,
address outputToken,
uint256 inputAmountSent,
uint256 totalOutputAmount,
bytes calldata takeOrdersData
) public virtual override;
fallback
Allow receiving gas.
fallback() external onlyNotInitializing;
Constants
CALLER_META_HASH
bytes32 constant CALLER_META_HASH = bytes32(0xb5923e408032e4f76298ae26c496091f2d8f16a7c968bbd5ee83bf90b5aca00e);
Contents
Contents
- DepositConfig
- WithdrawConfig
- IO
- OrderConfig
- Order
- TakeOrdersConfig
- TakeOrderConfig
- ClearConfig
- ClearStateChange
- IOrderBookV1
- DepositConfig
- WithdrawConfig
- IO
- OrderConfig
- Order
- TakeOrdersConfig
- TakeOrderConfig
- ClearConfig
- ClearStateChange
- IOrderBookV2
DepositConfig
Configuration for a deposit. All deposits are processed by and for
msg.sender so the vaults are unambiguous here.
struct DepositConfig {
address token;
uint256 vaultId;
uint256 amount;
}
Properties
| Name | Type | Description |
|---|---|---|
token | address | The token to deposit. |
vaultId | uint256 | The vault ID for the token to deposit. |
amount | uint256 | The amount of the token to deposit. |
WithdrawConfig
Configuration for a withdrawal. All withdrawals are processed by and for
msg.sender so the vaults are unambiguous here.
struct WithdrawConfig {
address token;
uint256 vaultId;
uint256 amount;
}
Properties
| Name | Type | Description |
|---|---|---|
token | address | The token to withdraw. |
vaultId | uint256 | The vault ID for the token to withdraw. |
amount | uint256 | The amount of the token to withdraw. |
IO
Configuration for a single input or output on an Order.
struct IO {
address token;
uint8 decimals;
uint256 vaultId;
}
Properties
| Name | Type | Description |
|---|---|---|
token | address | The token to either send from the owner as an output or receive from the counterparty to the owner as an input. The tokens are not moved during an order, only internal vault balances are updated, until a separate withdraw step. |
decimals | uint8 | The decimals to use for internal scaling calculations for token. This is provided directly in IO to save gas on external lookups and to respect the ERC20 spec that mandates NOT assuming or using the decimals method for onchain calculations. Ostensibly the decimals exists so that all calculate order entrypoints can treat amounts and ratios as 18 decimal fixed point values. Order max amounts MUST be rounded down and IO ratios rounded up to compensate for any loss of precision during decimal rescaling. |
vaultId | uint256 | The vault ID that tokens will move into if this is an input or move out from if this is an output. |
OrderConfig
Config the order owner may provide to define their order. The msg.sender
that adds an order cannot modify the owner nor bypass the integrity check of
the expression deployer that they specify. However they MAY specify a
deployer with a corrupt integrity check, so counterparties and clearers MUST
check the DISpair of the order and avoid untrusted pairings.
struct OrderConfig {
IO[] validInputs;
IO[] validOutputs;
EvaluableConfig evaluableConfig;
bytes meta;
}
Properties
| Name | Type | Description |
|---|---|---|
validInputs | IO[] | As per validInputs on the Order. |
validOutputs | IO[] | As per validOutputs on the Order. |
evaluableConfig | EvaluableConfig | Standard EvaluableConfig used to produce the Evaluable on the order. |
meta | bytes | Arbitrary bytes that will NOT be used in the order evaluation but MUST be emitted as a Rain MetaV1 when the order is placed so can be used by offchain processes. |
Order
Defines a fully deployed order ready to evaluate by Orderbook.
struct Order {
address owner;
bool handleIO;
Evaluable evaluable;
IO[] validInputs;
IO[] validOutputs;
}
Properties
| Name | Type | Description |
|---|---|---|
owner | address | The owner of the order is the msg.sender that added the order. |
handleIO | bool | true if there is a "handle IO" entrypoint to run. If false the order book MAY skip calling the interpreter to save gas. |
evaluable | Evaluable | Standard Evaluable with entrypoints for both "calculate order" and "handle IO". The latter MAY be empty bytes, in which case it will be skipped at runtime to save gas. |
validInputs | IO[] | A list of input tokens that are economically equivalent for the purpose of processing this order. Inputs are relative to the order so these tokens will be sent to the owners vault. |
validOutputs | IO[] | A list of output tokens that are economically equivalent for the purpose of processing this order. Outputs are relative to the order so these tokens will be sent from the owners vault. |
TakeOrdersConfig
Config for a list of orders to take sequentially as part of a takeOrders
call.
struct TakeOrdersConfig {
address output;
address input;
uint256 minimumInput;
uint256 maximumInput;
uint256 maximumIORatio;
TakeOrderConfig[] orders;
}
Properties
| Name | Type | Description |
|---|---|---|
output | address | Output token from the perspective of the order taker. |
input | address | Input token from the perspective of the order taker. |
minimumInput | uint256 | Minimum input from the perspective of the order taker. |
maximumInput | uint256 | Maximum input from the perspective of the order taker. |
maximumIORatio | uint256 | Maximum IO ratio as calculated by the order being taken. The input is from the perspective of the order so higher ratio means worse deal for the order taker. |
orders | TakeOrderConfig[] | Ordered list of orders that will be taken until the limit is hit. Takers are expected to prioritise orders that appear to be offering better deals i.e. lower IO ratios. This prioritisation and sorting MUST happen offchain, e.g. via. some simulator. |
TakeOrderConfig
Config for an individual take order from the overall list of orders in a
call to takeOrders.
struct TakeOrderConfig {
Order order;
uint256 inputIOIndex;
uint256 outputIOIndex;
SignedContext[] signedContext;
}
Properties
| Name | Type | Description |
|---|---|---|
order | Order | The order being taken this iteration. |
inputIOIndex | uint256 | The index of the input token in order to match with the take order output. |
outputIOIndex | uint256 | The index of the output token in order to match with the take order input. |
signedContext | SignedContext[] | Optional additional signed context relevant to the taken order. |
ClearConfig
Additional config to a clear that allows two orders to be fully matched to
a specific token moment. Also defines the bounty for the clearer.
struct ClearConfig {
uint256 aliceInputIOIndex;
uint256 aliceOutputIOIndex;
uint256 bobInputIOIndex;
uint256 bobOutputIOIndex;
uint256 aliceBountyVaultId;
uint256 bobBountyVaultId;
}
Properties
| Name | Type | Description |
|---|---|---|
aliceInputIOIndex | uint256 | The index of the input token in order A. |
aliceOutputIOIndex | uint256 | The index of the output token in order A. |
bobInputIOIndex | uint256 | The index of the input token in order B. |
bobOutputIOIndex | uint256 | The index of the output token in order B. |
aliceBountyVaultId | uint256 | The vault ID that the bounty from order A should move to for the clearer. |
bobBountyVaultId | uint256 | The vault ID that the bounty from order B should move to for the clearer. |
ClearStateChange
Summary of the vault state changes due to clearing an order. NOT the state changes sent to the interpreter store, these are the LOCAL CHANGES in vault balances. Note that the difference in inputs/outputs overall between the counterparties is the bounty paid to the entity that cleared the order.
struct ClearStateChange {
uint256 aliceOutput;
uint256 bobOutput;
uint256 aliceInput;
uint256 bobInput;
}
Properties
| Name | Type | Description |
|---|---|---|
aliceOutput | uint256 | Amount of counterparty A's output token that moved out of their vault. |
bobOutput | uint256 | Amount of counterparty B's output token that moved out of their vault. |
aliceInput | uint256 | Amount of counterparty A's input token that moved into their vault. |
bobInput | uint256 | Amount of counterparty B's input token that moved into their vault. |
IOrderBookV1
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
| 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,
SignedContext[] memory aliceSignedContext,
SignedContext[] 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 | SignedContext[] | Optional signed context that is relevant to A. |
bobSignedContext | SignedContext[] | 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, IExpressionDeployerV3 expressionDeployer, Order order, uint256 orderHash);
Parameters
| Name | Type | Description |
|---|---|---|
sender | address | msg.sender adding the order and is owner of the order. |
expressionDeployer | IExpressionDeployerV3 | 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. |
DepositConfig
Configuration for a deposit. All deposits are processed by and for
msg.sender so the vaults are unambiguous here.
struct DepositConfig {
address token;
uint256 vaultId;
uint256 amount;
}
Properties
| Name | Type | Description |
|---|---|---|
token | address | The token to deposit. |
vaultId | uint256 | The vault ID for the token to deposit. |
amount | uint256 | The amount of the token to deposit. |
WithdrawConfig
Configuration for a withdrawal. All withdrawals are processed by and for
msg.sender so the vaults are unambiguous here.
struct WithdrawConfig {
address token;
uint256 vaultId;
uint256 amount;
}
Properties
| Name | Type | Description |
|---|---|---|
token | address | The token to withdraw. |
vaultId | uint256 | The vault ID for the token to withdraw. |
amount | uint256 | The amount of the token to withdraw. |
IO
Configuration for a single input or output on an Order.
struct IO {
address token;
uint8 decimals;
uint256 vaultId;
}
Properties
| Name | Type | Description |
|---|---|---|
token | address | The token to either send from the owner as an output or receive from the counterparty to the owner as an input. The tokens are not moved during an order, only internal vault balances are updated, until a separate withdraw step. |
decimals | uint8 | The decimals to use for internal scaling calculations for token. This is provided directly in IO to save gas on external lookups and to respect the ERC20 spec that mandates NOT assuming or using the decimals method for onchain calculations. Ostensibly the decimals exists so that all calculate order entrypoints can treat amounts and ratios as 18 decimal fixed point values. Order max amounts MUST be rounded down and IO ratios rounded up to compensate for any loss of precision during decimal rescaling. |
vaultId | uint256 | The vault ID that tokens will move into if this is an input or move out from if this is an output. |
OrderConfig
Config the order owner may provide to define their order. The msg.sender
that adds an order cannot modify the owner nor bypass the integrity check of
the expression deployer that they specify. However they MAY specify a
deployer with a corrupt integrity check, so counterparties and clearers MUST
check the DISpair of the order and avoid untrusted pairings.
struct OrderConfig {
IO[] validInputs;
IO[] validOutputs;
EvaluableConfig evaluableConfig;
bytes meta;
}
Properties
| Name | Type | Description |
|---|---|---|
validInputs | IO[] | As per validInputs on the Order. |
validOutputs | IO[] | As per validOutputs on the Order. |
evaluableConfig | EvaluableConfig | Standard EvaluableConfig used to produce the Evaluable on the order. |
meta | bytes | Arbitrary bytes that will NOT be used in the order evaluation but MUST be emitted as a Rain MetaV1 when the order is placed so can be used by offchain processes. |
Order
Defines a fully deployed order ready to evaluate by Orderbook.
struct Order {
address owner;
bool handleIO;
Evaluable evaluable;
IO[] validInputs;
IO[] validOutputs;
}
Properties
| Name | Type | Description |
|---|---|---|
owner | address | The owner of the order is the msg.sender that added the order. |
handleIO | bool | true if there is a "handle IO" entrypoint to run. If false the order book MAY skip calling the interpreter to save gas. |
evaluable | Evaluable | Standard Evaluable with entrypoints for both "calculate order" and "handle IO". The latter MAY be empty bytes, in which case it will be skipped at runtime to save gas. |
validInputs | IO[] | A list of input tokens that are economically equivalent for the purpose of processing this order. Inputs are relative to the order so these tokens will be sent to the owners vault. |
validOutputs | IO[] | A list of output tokens that are economically equivalent for the purpose of processing this order. Outputs are relative to the order so these tokens will be sent from the owners vault. |
TakeOrdersConfig
Config for a list of orders to take sequentially as part of a takeOrders
call.
struct TakeOrdersConfig {
address output;
address input;
uint256 minimumInput;
uint256 maximumInput;
uint256 maximumIORatio;
TakeOrderConfig[] orders;
}
Properties
| Name | Type | Description |
|---|---|---|
output | address | Output token from the perspective of the order taker. |
input | address | Input token from the perspective of the order taker. |
minimumInput | uint256 | Minimum input from the perspective of the order taker. |
maximumInput | uint256 | Maximum input from the perspective of the order taker. |
maximumIORatio | uint256 | Maximum IO ratio as calculated by the order being taken. The input is from the perspective of the order so higher ratio means worse deal for the order taker. |
orders | TakeOrderConfig[] | Ordered list of orders that will be taken until the limit is hit. Takers are expected to prioritise orders that appear to be offering better deals i.e. lower IO ratios. This prioritisation and sorting MUST happen offchain, e.g. via. some simulator. |
TakeOrderConfig
Config for an individual take order from the overall list of orders in a
call to takeOrders.
struct TakeOrderConfig {
Order order;
uint256 inputIOIndex;
uint256 outputIOIndex;
SignedContextV1[] signedContext;
}
Properties
| Name | Type | Description |
|---|---|---|
order | Order | The order being taken this iteration. |
inputIOIndex | uint256 | The index of the input token in order to match with the take order output. |
outputIOIndex | uint256 | The index of the output token in order to match with the take order input. |
signedContext | SignedContextV1[] | Optional additional signed context relevant to the taken order. |
ClearConfig
Additional config to a clear that allows two orders to be fully matched to
a specific token moment. Also defines the bounty for the clearer.
struct ClearConfig {
uint256 aliceInputIOIndex;
uint256 aliceOutputIOIndex;
uint256 bobInputIOIndex;
uint256 bobOutputIOIndex;
uint256 aliceBountyVaultId;
uint256 bobBountyVaultId;
}
Properties
| Name | Type | Description |
|---|---|---|
aliceInputIOIndex | uint256 | The index of the input token in order A. |
aliceOutputIOIndex | uint256 | The index of the output token in order A. |
bobInputIOIndex | uint256 | The index of the input token in order B. |
bobOutputIOIndex | uint256 | The index of the output token in order B. |
aliceBountyVaultId | uint256 | The vault ID that the bounty from order A should move to for the clearer. |
bobBountyVaultId | uint256 | The vault ID that the bounty from order B should move to for the clearer. |
ClearStateChange
Summary of the vault state changes due to clearing an order. NOT the state changes sent to the interpreter store, these are the LOCAL CHANGES in vault balances. Note that the difference in inputs/outputs overall between the counterparties is the bounty paid to the entity that cleared the order.
struct ClearStateChange {
uint256 aliceOutput;
uint256 bobOutput;
uint256 aliceInput;
uint256 bobInput;
}
Properties
| Name | Type | Description |
|---|---|---|
aliceOutput | uint256 | Amount of counterparty A's output token that moved out of their vault. |
bobOutput | uint256 | Amount of counterparty B's output token that moved out of their vault. |
aliceInput | uint256 | Amount of counterparty A's input token that moved into their vault. |
bobInput | uint256 | Amount of counterparty B's input token that moved into their vault. |
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. |
Contents
IERC3156FlashBorrower
Functions
onFlashLoan
Receive a flash loan.
function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata data)
external
returns (bytes32);
Parameters
| Name | Type | Description |
|---|---|---|
initiator | address | The initiator of the loan. |
token | address | The loan currency. |
amount | uint256 | The amount of tokens lent. |
fee | uint256 | The additional amount of tokens to repay. |
data | bytes | Arbitrary data structure, intended to contain user-defined parameters. |
Returns
| Name | Type | Description |
|---|---|---|
<none> | bytes32 | The keccak256 hash of "ERC3156FlashBorrower.onFlashLoan" |
Constants
ON_FLASH_LOAN_CALLBACK_SUCCESS
The ERC3156 spec mandates this hash be returned by onFlashLoan if it
succeeds.
bytes32 constant ON_FLASH_LOAN_CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
IERC3156FlashLender
Functions
maxFlashLoan
The amount of currency available to be lent.
function maxFlashLoan(address token) external view returns (uint256);
Parameters
| Name | Type | Description |
|---|---|---|
token | address | The loan currency. |
Returns
| Name | Type | Description |
|---|---|---|
<none> | uint256 | The amount of token that can be borrowed. |
flashFee
The fee to be charged for a given loan.
function flashFee(address token, uint256 amount) external view returns (uint256);
Parameters
| Name | Type | Description |
|---|---|---|
token | address | The loan currency. |
amount | uint256 | The amount of tokens lent. |
Returns
| Name | Type | Description |
|---|---|---|
<none> | uint256 | The amount of token to be charged for the loan, on top of the returned principal. |
flashLoan
Initiate a flash loan.
function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data)
external
returns (bool);
Parameters
| Name | Type | Description |
|---|---|---|
receiver | IERC3156FlashBorrower | The receiver of the tokens in the loan, and the receiver of the callback. |
token | address | The loan currency. |
amount | uint256 | The amount of tokens lent. |
data | bytes | Arbitrary data structure, intended to contain user-defined parameters. |
Contents
- NoOrders
- ZeroMaximumInput
- OrderConfigV2
- TakeOrderConfigV2
- OrderV2
- TakeOrdersConfigV2
- IOrderBookV3
- IOrderBookV3ArbOrderTaker
- IOrderBookV3OrderTaker
NoOrders
Import unmodified structures from older versions of IOrderBook.
Thrown when take orders is called with no orders.
error NoOrders();
ZeroMaximumInput
Thrown when take orders is called with a zero maximum input.
error ZeroMaximumInput();
OrderConfigV2
Config the order owner may provide to define their order. The msg.sender
that adds an order cannot modify the owner nor bypass the integrity check of
the expression deployer that they specify. However they MAY specify a
deployer with a corrupt integrity check, so counterparties and clearers MUST
check the DISpair of the order and avoid untrusted pairings.
struct OrderConfigV2 {
IO[] validInputs;
IO[] validOutputs;
EvaluableConfigV3 evaluableConfig;
bytes meta;
}
Properties
| Name | Type | Description |
|---|---|---|
validInputs | IO[] | As per validInputs on the Order. |
validOutputs | IO[] | As per validOutputs on the Order. |
evaluableConfig | EvaluableConfigV3 | Standard EvaluableConfig used to produce the Evaluable on the order. |
meta | bytes | Arbitrary bytes that will NOT be used in the order evaluation but MUST be emitted as a Rain MetaV1 when the order is placed so can be used by offchain processes. |
TakeOrderConfigV2
Config for an individual take order from the overall list of orders in a
call to takeOrders.
struct TakeOrderConfigV2 {
OrderV2 order;
uint256 inputIOIndex;
uint256 outputIOIndex;
SignedContextV1[] signedContext;
}
Properties
| Name | Type | Description |
|---|---|---|
order | OrderV2 | The order being taken this iteration. |
inputIOIndex | uint256 | The index of the input token in order to match with the take order output. |
outputIOIndex | uint256 | The index of the output token in order to match with the take order input. |
signedContext | SignedContextV1[] | Optional additional signed context relevant to the taken order. |
OrderV2
Defines a fully deployed order ready to evaluate by Orderbook. Identical to
Order except for the newer EvaluableV2.
struct OrderV2 {
address owner;
bool handleIO;
EvaluableV2 evaluable;
IO[] validInputs;
IO[] validOutputs;
}
Properties
| Name | Type | Description |
|---|---|---|
owner | address | The owner of the order is the msg.sender that added the order. |
handleIO | bool | true if there is a "handle IO" entrypoint to run. If false the order book MAY skip calling the interpreter to save gas. |
evaluable | EvaluableV2 | Standard EvaluableV2 with entrypoints for both "calculate order" and "handle IO". The latter MAY be empty bytes, in which case it will be skipped at runtime to save gas. |
validInputs | IO[] | A list of input tokens that are economically equivalent for the purpose of processing this order. Inputs are relative to the order so these tokens will be sent to the owners vault. |
validOutputs | IO[] | A list of output tokens that are economically equivalent for the purpose of processing this order. Outputs are relative to the order so these tokens will be sent from the owners vault. |
TakeOrdersConfigV2
Config for a list of orders to take sequentially as part of a takeOrders
call.
struct TakeOrdersConfigV2 {
uint256 minimumInput;
uint256 maximumInput;
uint256 maximumIORatio;
TakeOrderConfigV2[] orders;
bytes data;
}
Properties
| Name | Type | Description |
|---|---|---|
minimumInput | uint256 | Minimum input from the perspective of the order taker. |
maximumInput | uint256 | Maximum input from the perspective of the order taker. |
maximumIORatio | uint256 | Maximum IO ratio as calculated by the order being taken. The input is from the perspective of the order so higher ratio means worse deal for the order taker. |
orders | TakeOrderConfigV2[] | Ordered list of orders that will be taken until the limit is hit. Takers are expected to prioritise orders that appear to be offering better deals i.e. lower IO ratios. This prioritisation and sorting MUST happen offchain, e.g. via. some simulator. |
data | bytes | If nonzero length, triggers onTakeOrders on the caller of takeOrders with this data. This allows the caller to perform arbitrary onchain actions between receiving their input tokens, before having to send their output tokens. |
IOrderBookV3
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.
Main differences between IOrderBookV2 and IOderBookV3:
- Most structs are now primitives to save gas.
- Order hash is
bytes32. depositandwithdrawMUST revert if the amount is zero.- adding an order MUST revert if there is no calculation entrypoint.
- adding an order MUST revert if there is no handle IO entrypoint.
- adding an order MUST revert if there are no inputs.
- adding an order MUST revert if there are no outputs.
- adding and removing orders MUST return a boolean indicating if the state changed.
- new
orderExistsmethod.
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.
Vault IDs are namespaced by the token address so there is no risk of
collision between tokens. For example, vault ID 0 for token A is
completely different to vault ID 0 for token B.
0 amount deposits are unsupported as underlying token contracts
handle 0 value transfers differently and this would be a source of
confusion. The order book MUST revert with ZeroDepositAmount if the
amount is zero.
function deposit(address token, uint256 vaultId, uint256 amount) external;
Parameters
| Name | Type | Description |
|---|---|---|
token | address | The token to deposit. |
vaultId | uint256 | The vault ID to deposit under. |
amount | uint256 | The amount of tokens to 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. MUST revert if the amount requested to withdraw is zero. The withdrawal MAY still not move any tokens (without revert) if the vault balance is zero, or the withdrawal is used to repay a flash loan, or due to any other internal accounting.
function withdraw(address token, uint256 vaultId, uint256 targetAmount) external;
Parameters
| Name | Type | Description |
|---|---|---|
token | address | The token to withdraw. |
vaultId | uint256 | The vault ID to withdraw from. |
targetAmount | uint256 | The amount of tokens to attempt to withdraw. MAY result in fewer tokens withdrawn if the vault balance is lower than the target amount. MAY NOT be zero, the order book MUST revert with ZeroWithdrawTargetAmount if the amount is zero. |
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.
MUST revert with OrderNoSources if the order has no associated
calculation and OrderNoHandleIO if the order has no handle IO
entrypoint. The calculation MUST return at least two values from
evaluation, the maximum amount and the IO ratio. The handle IO entrypoint
SHOULD return zero values from evaluation. Either MAY revert during
evaluation on the interpreter, which MUST prevent the order from
clearing.
MUST revert with OrderNoInputs if the order has no inputs.
MUST revert with OrderNoOutputs if the order has no outputs.
If the order already exists, the order book MUST NOT change state, which
includes not emitting an event. Instead it MUST return false. If the
order book modifies state it MUST emit an AddOrder event and return
true.
function addOrder(OrderConfigV2 calldata config) external returns (bool stateChanged);
Parameters
| Name | Type | Description |
|---|---|---|
config | OrderConfigV2 | All config required to build an Order. |
Returns
| Name | Type | Description |
|---|---|---|
stateChanged | bool | True if the order was added, false if it already existed. |
orderExists
Returns true if the order exists, false otherwise.
function orderExists(bytes32 orderHash) external view returns (bool exists);
Parameters
| Name | Type | Description |
|---|---|---|
orderHash | bytes32 | The hash of the order to check. |
Returns
| Name | Type | Description |
|---|---|---|
exists | bool | True if the order exists, false otherwise. |
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(OrderV2 calldata order) external returns (bool stateChanged);
Parameters
| Name | Type | Description |
|---|---|---|
order | OrderV2 | The Order data exactly as it was added. |
Returns
| Name | Type | Description |
|---|---|---|
stateChanged | bool | True if the order was removed, false if it did not exist. |
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(TakeOrdersConfigV2 calldata config) external returns (uint256 totalInput, uint256 totalOutput);
Parameters
| Name | Type | Description |
|---|---|---|
config | TakeOrdersConfigV2 | 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(
OrderV2 memory alice,
OrderV2 memory bob,
ClearConfig calldata clearConfig,
SignedContextV1[] memory aliceSignedContext,
SignedContextV1[] memory bobSignedContext
) external;
Parameters
| Name | Type | Description |
|---|---|---|
alice | OrderV2 | Some order to clear. |
bob | OrderV2 | 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, address token, uint256 vaultId, uint256 amount);
Parameters
| Name | Type | Description |
|---|---|---|
sender | address | msg.sender depositing tokens. Delegated deposits are NOT supported. |
token | address | The token being deposited. |
vaultId | uint256 | The vault ID the tokens are being deposited under. |
amount | uint256 | The amount of tokens deposited. |
Withdraw
Some tokens have been withdrawn from a vault.
event Withdraw(address sender, address token, uint256 vaultId, uint256 targetAmount, uint256 amount);
Parameters
| Name | Type | Description |
|---|---|---|
sender | address | msg.sender withdrawing tokens. Delegated withdrawals are NOT supported. |
token | address | The token being withdrawn. |
vaultId | uint256 | The vault ID the tokens are being withdrawn from. |
targetAmount | uint256 | The amount of tokens requested to withdraw. |
amount | uint256 | The amount of tokens withdrawn, can be less than the target amount if the vault does not have the funds available to cover the target 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, OrderV2 order, bytes32 orderHash);
Parameters
| Name | Type | Description |
|---|---|---|
sender | address | msg.sender adding the order and is owner of the order. |
expressionDeployer | IExpressionDeployerV3 | 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 | OrderV2 | 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 | bytes32 | 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, OrderV2 order, bytes32 orderHash);
Parameters
| Name | Type | Description |
|---|---|---|
sender | address | msg.sender removing the order and is owner of the order. |
order | OrderV2 | The removed order. |
orderHash | bytes32 | 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, TakeOrderConfigV2 config, uint256 input, uint256 output);
Parameters
| Name | Type | Description |
|---|---|---|
sender | address | msg.sender taking the orders. |
config | TakeOrderConfigV2 | 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, bytes32 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 | bytes32 | 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, bytes32 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 | bytes32 | 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, bytes32 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 | bytes32 | 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, OrderV2 alice, OrderV2 bob, ClearConfig clearConfig);
Parameters
| Name | Type | Description |
|---|---|---|
sender | address | msg.sender clearing both orders. |
alice | OrderV2 | One of the orders. |
bob | OrderV2 | 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. |
Errors
ZeroDepositAmount
MUST be thrown by deposit if the amount is zero.
error ZeroDepositAmount(address sender, address token, uint256 vaultId);
Parameters
| Name | Type | Description |
|---|---|---|
sender | address | msg.sender depositing tokens. |
token | address | The token being deposited. |
vaultId | uint256 | The vault ID the tokens are being deposited under. |
ZeroWithdrawTargetAmount
MUST be thrown by withdraw if the amount requested to withdraw is
zero. The withdrawal MAY still not move any tokens if the vault balance
is zero, or the withdrawal is used to repay a flash loan.
error ZeroWithdrawTargetAmount(address sender, address token, uint256 vaultId);
Parameters
| Name | Type | Description |
|---|---|---|
sender | address | msg.sender withdrawing tokens. |
token | address | The token being withdrawn. |
vaultId | uint256 | The vault ID the tokens are being withdrawn from. |
OrderNoSources
MUST be thrown by addOrder if the order has no associated calculation.
error OrderNoSources();
OrderNoHandleIO
MUST be thrown by addOrder if the order has no associated handle IO.
error OrderNoHandleIO();
OrderNoInputs
MUST be thrown by addOrder if the order has no inputs.
error OrderNoInputs();
OrderNoOutputs
MUST be thrown by addOrder if the order has no outputs.
error OrderNoOutputs();
IOrderBookV3ArbOrderTaker
Inherits: IOrderBookV3OrderTaker
Functions
arb
function arb(TakeOrdersConfigV2 calldata takeOrders, uint256 minimumSenderOutput) external payable;
IOrderBookV3OrderTaker
Functions
onTakeOrders
Called by OrderBookV3 when takeOrders is called with non-zero
data, if it caused a non-zero input amount. I.e. if the order(s) taker
received some tokens. Input and output directions are relative to the
IOrderBookV3OrderTaker contract. If the order(s) taker had an active
debt from a flash loan then that debt will be paid before calculating
any input amounts sent.
i.e. the debt is deducted from the input amount before this callback is
called.
function onTakeOrders(
address inputToken,
address outputToken,
uint256 inputAmountSent,
uint256 totalOutputAmount,
bytes calldata takeOrdersData
) external;
Parameters
| Name | Type | Description |
|---|---|---|
inputToken | address | The token that was sent to IOrderBookV3OrderTaker. |
outputToken | address | The token that IOrderBookV3 will attempt to pull from IOrderBookV3OrderTaker after this callback returns. |
inputAmountSent | uint256 | The amount of inputToken that was sent to IOrderBookV3OrderTaker. |
totalOutputAmount | uint256 | The total amount of outputToken that IOrderBookV3 will attempt to pull from IOrderBookV3OrderTaker after this callback returns. |
takeOrdersData | bytes | The data passed to takeOrders by the caller. |
Contents
LibOrder
Consistent handling of OrderV2 for where it matters w.r.t.
determinism and security.
Functions
hash
Hashes OrderV2 in a secure and deterministic way. Uses abi.encode
rather than abi.encodePacked to guard against potential collisions where
many inputs encode to the same output bytes.
function hash(OrderV2 memory order) internal pure returns (bytes32);
Parameters
| Name | Type | Description |
|---|---|---|
order | OrderV2 | The order to hash. |
Returns
| Name | Type | Description |
|---|---|---|
<none> | bytes32 | The hash of order. |