Current Book and History
The book is fully on-chain and thus it can be obtained directly through an eth_call to a periphery reader contract. However, if needed, it can also be constructed off-chain from either an initial state or genesis. For this purpose, we need to build a synthetic clone of the book off-chain. Only by creating this off-chain clone would we be able to construct the full history of the book.
Offers
We already saw how to link offer IDs and makers in the Offer Owners section. This link is immutable. An offer consists of the following fields:
| name | type | description | mutable |
|---|---|---|---|
| offer ID (pk) | uint40 | The offer ID, defined below. | ❌ |
| market ID (pk) | bytes32 | The market ID on which the offer is consumed. | ❌ |
| maker | address | The owner of the offer. | ❌ |
| gives | uint256 | The amount of outbound token that the maker is selling to the market. | ✅ |
| received | uint256 | The amount of inbound token that the maker is receiving from the market. | ✅ |
| tick | int24 | The tick at which the offer is placed. | ✅ |
| expiry | uint32 | The expiry of the offer (0 if undefined). | ✅ |
| provision | uint24 | The provision locked by the maker that will be used to pay the taker in case of expiration (defined in gwei). | ✅ |
| active | bool | The active status of the offer. | ✅ |
Snapshot Entities
Snapshot entities will refer to value snapshots that could be edition snapshots, matching snapshots, or claiming/deletion snapshots. These should have a composite foreign key of the offer market and offerId pair, and have a composite primary key of the block and event index. These should contain the full timed metadata of the snapshot.
An index can be created on the timestamp which will likely be used frequently while querying the history of an offer.
Some snapshot suggestions would be:
Edition Snapshots
This snapshot would be created upon offer creation or edition. This snapshot assumes that received is set to 0 in all cases.
| name | type | description |
|---|---|---|
| offer ID (fk) | uint40 | The offer ID. |
| market ID (fk) | bytes32 | The market ID on which the offer is consumed. |
| gives | uint256 | The new amount to sell |
| tick | int24 | The new tick at which the offer is placed. |
| expiry | uint32 | The new expiry of the offer (0 if undefined). |
| provision | uint24 | The new provision (defined in gwei). |
| timed metadata (pk) | The timed metadata of the snapshot (contains the composite pk). |
Deletion/Claiming Snapshots
This snapshot would be created upon offer deletion/claiming.
| name | type | description |
|---|---|---|
| offer ID (fk) | uint40 | The offer ID. |
| market ID (fk) | bytes32 | The market ID on which the offer is consumed. |
| type | enum | either deletion or claiming |
| outbound | uint256 | The amount of outbound token claimed |
| inbound | uint256 | The amount of inbound token claimed |
| provision | uint24 | The provision claimed (defined in wei). |
| timed metadata (pk) | The timed metadata of the snapshot (contains the composite pk). |
Cleaning Snapshots
This snapshot would be created upon offer cleaning. Since an offer can also be cleaned during the matching process, as well as many other offers on a single event, the primary key must be extended to include the offer ID and the market ID.
| name | type | description |
|---|---|---|
| offer ID (fk/pk) | uint40 | The offer ID. |
| market ID (fk/pk) | bytes32 | The market ID on which the offer is consumed. |
| bounty | uint256 | The amount of provision claimed. |
| timed metadata (pk) | The timed metadata of the snapshot (contains the composite pk). |
Matching Snapshots
This snapshot would be created upon offer matching. Since a single event will be linked to several offers being matched, but an offer can only be matched once per event, we need to extend the composite primary key to include the offer ID and the market ID.
| name | type | description |
|---|---|---|
| offer ID (fk/pk) | uint40 | The offer ID. |
| market ID (fk/pk) | bytes32 | The market ID on which the offer is consumed. |
| sent | uint256 | The amount of outbound token that the maker sent |
| received | uint256 | The amount of inbound token that the maker received |
| timed metadata (pk) | The timed metadata of the snapshot (contains the composite pk). |
Explicit Offer Indexing Events
An offer has a few explicit events that can be indexed like those reviewed before. All explicit events are linked to the offer being created, edited, claimed, or cancelled.
// ====== IVifMaking ======
/// @notice Emitted when a new offer is created.
event NewOffer(
bytes32 indexed market,
uint40 indexed offerId,
address indexed maker,
uint256 gives,
int24 tick,
uint32 expiry,
uint24 provision
);
/// @notice Emitted when an existing offer is updated.
event OfferUpdated(
bytes32 indexed market,
uint40 indexed offerId,
uint256 gives,
uint256 claimedReceived,
int24 tick,
uint32 expiry,
uint24 provision
);
/// @notice Emitted when an offer is cancelled.
event OfferCancelled(
bytes32 indexed market, uint40 indexed offerId, uint256 outbound, uint256 inbound, uint256 provision
);
/// @notice Emitted when an offer's claim is processed.
event OfferClaimed(
bytes32 indexed market, uint40 indexed offerId, uint256 outbound, uint256 inbound, uint256 provision
);
// ====== IVifTaking ======
/// @notice Emitted when an offer is cleaned.
event OfferCleaned(bytes32 indexed market, uint40 indexed offerId, uint256 bounty);Offer creation: NewOffer
The NewOffer event is fired when a new offer is created. This event will only be fired once per market and offerId pair, and will immutably link an offer to its maker. The remaining fields will populate the rest of the offer metadata.
When this event is fired, the active field is set to true.
We can create a new edition snapshot entity linked to our offer.
Offer update: OfferUpdated
The OfferUpdated event is fired when an offer is updated. All amounts in this event will override the previous values.
When this event is fired, the active field is set to true.
We can create a new edition snapshot entity linked to our offer.
When this event is fired, all received amounts are withdrawn to avoid overflows. The withdrawn amount is claimedReceived, denominated in inbound tokens.
Offer cleaning: OfferCleaned
The OfferCleaned event is fired when an offer is cleaned. The emitted amount is the provision taken for the cleaner.
The active field is set to false. The provision field must be subtracted by the emitted amount scaled down to gwei units (amounts will always be a multiple of gwei even though they are expressed in wei).
We can create a new deletion/claiming snapshot entity linked to our offer.
Offer cancellation: OfferCancelled
When an offer is cancelled, the active field is set to false. All mutable fields will be reset to 0. The emitted amounts are the amounts to be claimed. The amounts can be strictly checked if the matching is identical to the on-chain matching engine, or they can be loosely checked.
We can create a new deletion/claiming snapshot entity linked to our offer.
Offer claiming: OfferClaimed
The OfferClaimed event is fired when an offer is claimed. There are two scenarios:
- The offer is inactive and/or expired
The claiming will act as a cancellation, withdrawing all funds from the offer. All mutable fields can be reset to 0.
- The offer is active and not expired
Claiming will only claim the received amount. So we must only reset the received field to 0. The rest will remain the same.
The amounts emitted are the amounts withdrawn from the offer. inbound is always equal to the received field (adjusted by the units).
In scenario one, outbound is always equal to the gives field (adjusted by the units) and provision is always equal to the provision field (adjusted to gwei).
In scenario two, outbound and provision are always equal to 0.
Implicit offer indexing
Offer matching indexing is implicit, meaning we only index all offers matched from a single event:
/// @notice Emitted when a market order is executed.
event MarketOrder(
bytes32 indexed market,
address indexed taker,
uint256 got,
uint256 gave,
uint256 fee,
uint256 bounty,
uint256 fillVolume,
bool fillWants
);Rebuilding the data structure
First of all, we need to rebuild the full data representation of the book to rebuild the matching engine. The offers are matched via price priority and time priority. If an offer is edited, it goes to the end of the queue.
- When an offer is created, it is pushed to the queue at the given tick.
- When an offer is edited, it is removed from the queue of its previous tick and pushed to the queue of its new tick (even if the tick remains the same).
- When an offer is cancelled/cleaned, it is removed from the queue.
- When an offer
givesfalls below the minimum outbound amount (defined in the market parameters), it is removed from the queue.
Matching an offer
Now that we know in which order we need to consume offers, we can start matching them. When matching an offer, the first thing to do is to check if it is expired. If so, do the same as above with the cleaning section.
If not, then we will exchange with this offer. This part of the algorithm assumes that you have a remaining amount of token to receive/send. Depending on whether fillWants is true or false, we will match an offer differently.
We first need to define a few functions before writing the code. The first one is the function to get the virtual wants amount. This amount is the amount an offer is implicitly requesting. It is derived from give and tick. In order to define this function, refer to the Binary Exponentiation section for the computation of the price and the Tick section for the computation of the virtual wants amount. Let's call this function getWants. Here is a TypeScript implementation example:
/**
* Match an offer
* @param fillVolume - The amount of token to fill (expressed in units of the correct token)
* @param fillWants - Whether the fillVolume is the amount asked or the amount sent
* @param gives - The amount of token the offer is giving (in units)
* @param tick - The tick of the offer
* @param inboundUnits - The units of the inbound token
* @param outboundUnits - The units of the outbound token
* @returns The amount of token received and the amount of token sent (in units)
*/
function matchOffer(
fillVolume: bigint,
fillWants: boolean,
gives: bigint,
tick: bigint,
inboundUnits: bigint,
outboundUnits: bigint
): { received: bigint; send: bigint } {
const wants = getWants(gives, tick, inboundUnits, outboundUnits);
if (fillWants) {
if (fillVolume > gives) {
return { received: gives, send: wants };
}
return { received: fillVolume, send: divUp(wants * fillVolume, gives) };
}
// fillWants is false
if (fillVolume > wants) {
return { received: wants, send: gives };
}
return { received: divDown(gives * fillVolume, wants), send: fillVolume };
}Final Algorithm
Now let's assume that we have the data structure ready. When we receive a market order, we can execute the algorithm below. We need to save other fields, but for this part we'll only use these specific fields. The following is pseudocode, so you might need to adapt it to your needs or make your own implementation.
function executeMarketOrder(
fillWants: boolean,
got: bigint,
gave: bigint
) {
let offer = nextOffer();
let fillVolume = fillWants ? got : gave;
while (offer) {
if (offer.expired()) {
cleanOffer(offer);
offer = nextOffer();
continue;
}
const { received, send } = matchOffer(
fillVolume,
fillWants,
offer.gives,
offer.tick,
inboundUnits,
outboundUnits
);
saveOffer({
...offer,
gives: offer.gives - received,
received: offer.received + send,
});
saveOfferMatchSnapshot(offer, received, send);
handleRemoveOfferIfBelowMin(offer);
offer = nextOffer();
}
}