Skip to content

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:

nametypedescriptionmutable
offer ID (pk)uint40The offer ID, defined below.
market ID (pk)bytes32The market ID on which the offer is consumed.
makeraddressThe owner of the offer.
givesuint256The amount of outbound token that the maker is selling to the market.
receiveduint256The amount of inbound token that the maker is receiving from the market.
tickint24The tick at which the offer is placed.
expiryuint32The expiry of the offer (0 if undefined).
provisionuint24The provision locked by the maker that will be used to pay the taker in case of expiration (defined in gwei).
activeboolThe 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.

nametypedescription
offer ID (fk)uint40The offer ID.
market ID (fk)bytes32The market ID on which the offer is consumed.
givesuint256The new amount to sell
tickint24The new tick at which the offer is placed.
expiryuint32The new expiry of the offer (0 if undefined).
provisionuint24The 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.

nametypedescription
offer ID (fk)uint40The offer ID.
market ID (fk)bytes32The market ID on which the offer is consumed.
typeenumeither deletion or claiming
outbounduint256The amount of outbound token claimed
inbounduint256The amount of inbound token claimed
provisionuint24The 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.

nametypedescription
offer ID (fk/pk)uint40The offer ID.
market ID (fk/pk)bytes32The market ID on which the offer is consumed.
bountyuint256The 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.

nametypedescription
offer ID (fk/pk)uint40The offer ID.
market ID (fk/pk)bytes32The market ID on which the offer is consumed.
sentuint256The amount of outbound token that the maker sent
receiveduint256The 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:

  1. 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.

  1. 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 gives falls 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.ts
/**
 * 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();
  }
}