Skip to content
0xSlots

Hooks

A slot on its own is a position and nothing more. Holding it grants no rights and refuses no buyer, because there is nothing attached to decide either.

A hook is the one way to extend a slot — the ad that renders, the name that resolves, the feed you post to, the seven-day tenure nobody outbids you through. One interface does both jobs: what holding grants, and who may hold.

Set one at creation, or leave it empty for a bare, instant-buy slot.

The whole rule

before decides and may refuse. after records and cannot.

Every other property of a hook falls out of that one line:

  • before* callbacks are view. They cannot write, so they cannot reenter, so they are safe to call without a gas cap. Their only power is to revert — which vetoes the action. This is the "who may hold" half.
  • after* callbacks are gas-capped, and their revert is swallowed. They run after the action has settled and cannot change its outcome — so a broken hook can never block a buy, and above all can never block a liquidation, which the protocol treats as unconditional. This is the "what holding does" half: clear the ad, mint a receipt, update a ledger.
  • Unless the hook declares strict. Then its after* callbacks run uncapped and their revert propagates, so work that must land cannot be silently dropped — and the hook can fail the slot, eviction included. The declaration is snapshotted when the hook attaches and published in SlotInfo.hookFlags, so it is a fact about the slot rather than a surprise inside it.

There is deliberately no way to write during before. A hook that wants to record something about a decision does it in the matching after.

The six callbacks

One context struct — SlotContext — is passed to all of them; fields that aren't meaningful for a callback are zero.

CallbackKindFires
beforeBuydecidesomeone tries to buy the slot
beforeSelfAssessdecidethe occupant re-prices
afterBuyrecorda buy went through
afterReleaserecordthe occupant left
afterLiquidaterecordan insolvent occupant was removed
afterSettlerecordtax was charged
Loading diagram...

There is no beforeSell or afterSell. Slot.sell was removed — a consensual sale is selfAssess then buy, performed by the OfferBook — so a sale runs beforeSelfAssess and beforeBuy like any other seating. One seating path, one set of checks, and no second door for a hook to have forgotten.

A hook declares which callbacks it wants in a HookFlags struct, read once when it is attached and then snapshotted — never re-read. A hook that could widen its own reach mid-tenure could start charging an occupant gas they never agreed to.

Fail-closed, on purpose

A before hook that reverts, is broken, or is not a hook at all makes the action fail. A rule about who may take your property should never be skippable because a contract had a bad day.

Loading diagram...

The after side is the mirror image: capped and swallowed, so a broken effect degrades to "the slot grants nothing" rather than "the slot is frozen."

What no hook can do

A hook decides and records. It cannot move funds, change the price, or redirect the buyer — and two escape hatches bypass the before side entirely:

  • liquidate() — an insolvent occupant can always be removed. Otherwise a hook could keep someone in a slot they have stopped paying for.
  • release() — an occupant can always leave. Otherwise a hook could trap you in a position you no longer want.

The worst a bad hook can do is stop new people arriving. It can never strand money or trap a person.

Many behaviours: one hook

A slot points at exactly one hook, and the core makes one capped call into it per callback. To want several behaviours — a minimum tenure and a feed and a fee — you write one hook that does all three.

One contract, one stipend, one msg.sender: every callback costs a known maximum, and the slot is always the caller. A hook that needs configuration owns the slot's whole hookData word and decides for itself what the bytes mean.

Loading diagram...

Naming what a hook is

A hook is just an address; on its own it says nothing to a UI. A hook may optionally implement IDescribedHook, returning HookDescriptors that carry a family, a version, and a metadataURI for the human label ("7-day minimum tenure", not tenureSeconds: 604800).

Verifying a hook

Each hook kind ships with a factory that deploys it at a CREATE2 address derived from its terms, so verification is one call:

factory.verify(hook) // → bool

CREATE2 binds an address to the deployer, the init code and the salt, so only the genuine hook for those exact terms can sit there. That is why a client can name a hook it has never seen — it asks each known factory "did you make this?" rather than consulting a hardcoded list.

Next