Skip to content
0xSlots

Modules

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 module is the one way to extend a slot — the sponsor creative that renders, the NFT that follows the occupant, the seven-day tenure nobody outbids you through. A slot installs at most one, as ModuleTerms { module, settings }, at creation or later through its manager. 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 module falls out of that one line:

  • before* callbacks are view. The slot staticcalls them without a gas cap — a view cannot write, so it cannot reenter. Their only power is to revert, which vetoes the action, and the module's own error bubbles up.
  • after* callbacks are gas-capped and their revert is swallowed. They run after the action has settled, on MODULE_CALLBACK_GAS_LIMIT (500,000), and a failure is logged as ModuleCallFailed and otherwise ignored. A broken module can never block a buy, and above all can never block a liquidation.
  • Unless the module declares afterCallbacksMustSucceed. Then its after* callbacks run uncapped and their revert propagates, so work that must land — a mint, a transfer — cannot be silently dropped. The price is that the slot is only as evictable as the module.

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

Validators and hooks

Borrowing the vocabulary of ERC-7579, a module can be described by what its scopes let it do:

  • A module with before* scopes acts as a validator. It can refuse a buy or a self-assessment. View-only, uncapped.
  • A module with after* scopes acts as a hook. It records what happened. Gas-capped, and cannot block anything unless it declares afterCallbacksMustSucceed.

One module can be both — MinimumTenureModule and AdLand are. These are labels, not interfaces: every module implements the same ISlotModule, and a slot still installs exactly one.

The callbacks

A slot calls only the ones its module declared. One context struct — SlotContext — is passed to all of them; fields that aren't meaningful for a callback are zero.

CallbackActs asFires
beforeBuyvalidatorsomeone tries to buy the slot
beforeSelfAssessvalidatorthe occupant (or their operator) reprices
afterBuyhooka buy went through
afterReleasehookthe occupant left
afterLiquidatehookan insolvent occupant was removed
afterSettlehooktax was taken from the deposit
onInstallhookthe module became this slot's module
onUninstallhookthe module is being replaced or removed
Loading diagram...

onUninstall is always capped and swallowed, afterCallbacksMustSucceed or not: a module that could refuse its own removal could never be replaced.

There is no sell callback. A sale through the OfferBook is selfAssess then buy, so it runs beforeSelfAssess and beforeBuy like any other seating — one seating path, one set of checks.

Scopes and fee

A module says what it wants from a slot in two reads:

  • scopes(settings) — the callbacks it needs, as bits.
  • fee(settings) — an optional share of collected tax (bps, paid to recipient).

Both take the slot's settings, so the answer can depend on them: AdLand asks for its before scopes only when the config sets a tenure window.

The slot copies both when the module is installed and never re-reads them on a callback or a payout. The slot only calls the callbacks the copy lists — which matters for the before side: calling one on a contract that does not implement it would revert, and a reverting before vetoes every buy.

A module may later declare something different. The slot keeps its copy until the manager accepts the new value, and the two follow different rules:

  • acceptFee(expected) applies at once. It only changes how collected tax is split between the module and the recipient, never what an occupant pays, and tax collected so far is paid out under the old fee first. A fee can be anything up to all of the rent, but raising one — or attaching a module that charges at all — needs a slot whose recipient is mutable.
  • acceptScopes(expected) queues: new scopes change what the module may do to an occupant, so they land at the next buy, and only on a slot whose module is mutable.

A module proposed as a replacement is checked the same way. The slot keeps the scopes and fee it read at proposal in pending(), reads the module again when it lands, and drops it instead of installing it if either differs — so the delay cannot be used to raise a fee or add afterCallbacksMustSucceed after the manager looked.

Settings

settings are bytes stored on the slot and handed back in every callback as ctx.moduleTerms.settings: abi.encode of the fields the module's definition lists. It is what lets one deployment serve every configuration: a seven-day and a thirty-day minimum tenure point at the same MinimumTenureModule, each with its own window.

Because it lives on the slot, a slot whose module is immutable has both halves frozen — which module, and how it is configured. A module keeping the setting in its own storage could rewrite a slot's rules while the slot went on reporting itself immutable.

The slot asks validateSettings(settings) whenever a module is proposed or installed. A module that rejects its configuration is refused there, rather than installed and vetoing every buy afterwards. There is no size limit: large settings cost whoever builds and uses the slot, and they can see them first.

Fail-closed, on purpose

A validator that reverts — on purpose or because it is broken — 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 hook 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 module can do

A module decides and records. It cannot move funds, change the price, or redirect the buyer — and two exits never consult a validator:

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

Both still tell the module afterwards, and a afterCallbacksMustSucceed module can fail them — which is exactly why afterCallbacksMustSucceed is readable from the slot before anyone buys. Short of that, the worst a bad module can do is stop new people arriving.

Many behaviours, one module

A slot points at exactly one module, and the core makes one call into it per callback. To want several behaviours — a minimum tenure and a sponsor creative — you write one module that does both.

One contract, one gas budget, one msg.sender: every callback costs a known maximum, and the slot is always the caller. AdLand does this by inheriting the MinimumTenure rule rather than standing beside a second module.

Loading diagram...

Changing a slot's module

Only on a slot created with mutableModule, and only by its manager. The new ModuleTerms are validated when proposed, then wait out the terms delay and land at the next buy (or earlier, through applyTerms):

  1. the outgoing module gets onUninstall;
  2. the incoming module's scopes and fee are read afresh — one that no longer answers is dropped (ModuleDropped) rather than allowed to block the buy;
  3. the incoming module gets onInstall — on a buy, once the buyer is seated and before afterBuy.

Proposing ModuleTerms with a zero module removes the module.

Describing a module

A module is just an address; on its own it says nothing to a UI. It may optionally implement IModuleMetadata, whose metadata() returns a JSON document: a title, a description, and a JSON Schema for its settings — enough for a client to render a configuration form for a module nobody wrote a screen for.

To name the modules it knows, a client uses the catalogue in @0xslots/contracts — see Deployments.

Next