Skip to content
0xSlots

OfferBook

Standing bids on a slot, and the fill that settles them. Anyone may post "I will take that slot at this price"; the occupant reads the best one and accepts it.

The book is periphery. The core knows nothing about it — deploy another and it competes.

Why the book performs the fill

Slot.sell used to exist: the occupant submitted a bidder's EIP-712 order and the slot seated them. That was a second seating path — it reset the tenure like buy but ran beforeSell instead of beforeBuy, so every hook author had two doors to police, and two audit findings were the same mistake of policing one.

A consensual sale is now two calls the core already had, in one transaction:

slot.selfAssess(price)                 // the occupant's price, restated
slot.buy(bidder, price, deposit, max)  // the ordinary market path

The economics are unchanged — buy already refunds the outgoing occupant their deposit plus the price. What changed is that there is one seating path, one set of hook checks, and no order machinery in the slot.

Posting a bid

function offer(
    address slot,
    uint256 price,
    uint256 deposit,
    uint64  expiry
) external returns (uint256 id);
 
function cancel(address slot, uint256 id) external;   // bidder only

One transaction, and no signature: offer is already a call from the bidder, so posting is the consent. An offer used to carry an EIP-712 signature because Slot.sell demanded one; there is no second artefact left to disagree with the terms stored beside it.

Bids are allowance-backed, not escrowed. The book pulls the payment during the fill and spends it in the same transaction, so an allowance is as strong a guarantee at the only moment it matters — if the funds are gone when the occupant accepts, the transfer reverts and the seller loses gas and nothing else.

One offer per bidder per slot: posting again replaces your standing one. Two offers from one address are backed by the same allowance, so at most one could ever execute, and a stale high one would mask its owner's real intent.

Accepting a bid

function acceptOffer(address slot, uint256 id) external;   // occupant only

selfAssess is onlyOccupantOrOperator, so the occupant must make the book their operator first:

slot.setOperator(book, true);
book.acceptOffer(slot, id);

That grant is keyed by tenure on the slot's side, so it lapses by itself when the slot changes hands and cannot be inherited by the next occupant.

The hooks a slot has attached still get their say — beforeSelfAssess on the reprice and beforeBuy on the seating — and either may veto. That is the point of routing a sale through the market path rather than around it.

Errors worth handling

ErrorMeaning
NotOccupantonly the sitting occupant may accept a bid on their slot
NotOperatorthe book has not been granted setOperator for this tenure
OfferNotLivecancelled, expired, filled, unfunded, or the bidder is the occupant
NativeSlotNotSupportednative slots cannot be filled — see below
TopUpRequired(shortfall)accepting a higher bid raises the escrow floor above what the seller holds

TopUpRequired is the one that surprises people. Raising the declared price raises the minimum escrow with it, and selfAssess enforces that floor against the deposit already in the slot — the seller's, not the bidder's. Top up by the reported shortfall and retry; it comes straight back in the sale proceeds.

Reading the board

function board(address slot) external view
    returns (Offer[] memory offers, bool[] memory live);
 
function best(address slot) external view
    returns (bool found, uint256 id, Offer memory offer);
 
function liveCount(address slot) external view returns (uint256);
function offerCount(address slot) external view returns (uint256);
function isLive(address slot, uint256 id) external view returns (bool);
function isFundable(address slot, uint256 id) external view returns (bool);
function offerOf(address slot, address bidder) external view returns (…);
function offerAt(address slot, uint256 id) external view returns (Offer memory);
function offers(address slot) external view returns (Offer[] memory);

board returns the list and the contract's own per-entry liveness verdict in one call, which is why it is preferred over offers plus N isLive reads.

Events

event Offered(address indexed slot, address indexed bidder, uint256 indexed id,
              uint256 price, uint256 deposit, uint64 expiry);
event Cancelled(address indexed slot, address indexed bidder, uint256 indexed id);
event Filled(address indexed slot, address indexed bidder, uint256 indexed id,
             address seller, uint256 price, uint256 deposit);

Filled is the seller's side of a sale. The slot emits Bought in the same transaction, so occupancy history is complete from the slot alone.