Skip to content
0xSlots

Module reference

The interfaces a module implements, and the modules the protocol ships. See the concept for the model; this is the surface.

ISlotModule is required. IModuleMetadata is optional and client-only.

ISlotModule

interface ISlotModule {
    function validateSettings(bytes calldata settings) external view;
    function scopes(bytes calldata settings) external view returns (uint16);
    function fee(bytes calldata settings) external view returns (ModuleFee memory);
 
    // decisions — view, revert to veto
    function beforeBuy(SlotContext calldata ctx) external view;
    function beforeSelfAssess(SlotContext calldata ctx) external view;
 
    // effects — capped and swallowed, unless `afterCallbacksMustSucceed` is declared
    function afterBuy(SlotContext calldata ctx) external;
    function afterRelease(SlotContext calldata ctx) external;
    function afterLiquidate(SlotContext calldata ctx) external;
    function afterSettle(SlotContext calldata ctx) external;
    function onInstall(SlotContext calldata ctx) external;
    function onUninstall(SlotContext calldata ctx) external; // never fatal
}

A module implements every function; the slot calls only the callbacks its scopes declare.

validateSettings reverts if settings is not a configuration the module accepts. The slot calls it when the module is proposed and when it is installed. A module that takes no configuration implements it as a no-op and so accepts anything, empty included.

scopes must be non-zero and use only known bits. fee may be zero; a non-zero bps needs a recipient, may not exceed 10,000, and needs a slot whose recipient is mutable (NotMutable otherwise). Both are view, not pure — a module may answer from storage. Each read (validateSettings, scopes, fee) must answer within a third of MODULE_CALLBACK_GAS_LIMIT, which is what the slot allows when the module is installed; one that cannot is refused at proposal with ModuleTooExpensive.

SlotContext

One shape for every callback. Fields not meaningful for a given call are zero.

struct SlotContext {
    address slot;           // the slot calling — only trustworthy when msg.sender == slot
    address caller;         // who called the slot; not necessarily the occupant
    address account;        // incoming occupant on a transition; current otherwise
    address occupant;       // who holds it now; zero when vacant
    uint256 occupiedSince;  // when the current occupancy began; zero when vacant
    uint256 taxRateBps;     // basis points per 30 days
    uint256 currentPrice;
    uint256 newPrice;       // proposed in `before`, just set in `after`
    uint256 depositAmount;
    uint256 taxOwed;        // afterSettle only — tax that accrued
    uint256 taxPaid;        // afterSettle only — what the deposit could cover
    ModuleTerms moduleTerms; // this slot's module address and settings
}

taxOwed > taxPaid on afterSettle means the occupant has run dry. On afterRelease and afterLiquidate the context is built after the slot is vacated, so occupant, occupiedSince and currentPrice are zero and account is the occupant who left. beforeBuy runs after ripe queued terms have landed, so it judges the terms the buyer is seated under.

ModuleTerms

struct ModuleTerms {
    address module; // zero for none
    bytes settings; // this slot's configuration for it; must be empty when module is
}

ModuleFee and Scopes

struct ModuleFee {
    uint16  bps;          // share of collected tax, 0..10_000
    address recipient;    // required when bps is non-zero
}
 
struct Scopes {           // `scopes`, unpacked — what Slot.scopes() returns
    bool beforeBuy;
    bool beforeSelfAssess;
    bool afterBuy;
    bool afterRelease;
    bool afterLiquidate;
    bool afterSettle;
    bool afterCallbacksMustSucceed; // not a callback — a mode
    bool onInstall;
    bool onUninstall;
}
BitScopeBitScope
1 << 0beforeBuy1 << 5afterSettle
1 << 1beforeSelfAssess1 << 6afterCallbacksMustSucceed
1 << 2afterBuy1 << 7onInstall
1 << 3afterRelease1 << 8onUninstall
1 << 4afterLiquidate

afterCallbacksMustSucceed changes how the effect callbacks run: uncapped, and their revert propagates — eviction included. onUninstall stays capped and swallowed regardless. Leave afterCallbacksMustSucceed off unless a swallowed write would be worse than a stuck slot; the NFT modules below set it because they hold real ERC-721 ownership.

The slot's accepted copy is what counts: Slot.scopes() and Slot.fee(). How a manager accepts new ones is on the Slot reference.

IModuleMetadata

Optional. Self-reported metadata for clients — never read by the slot.

interface IModuleMetadata {
    function metadata() external pure returns (string memory);
}

metadata() returns one JSON document, built on-chain by ModuleSchemaLib from the module's own constants. MinimumTenureModule answers:

{
  "version": 1,
  "title": "Minimum tenure",
  "description": "Protects an occupant from being bought out for a fixed window after they take the slot.",
  "settings": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "title": "Minimum tenure",
    "type": "object",
    "properties": {
      "window": {
        "type": "string",
        "pattern": "^[0-9]+$",
        "title": "Minimum tenure",
        "description": "How long an occupant is protected from being bought out.",
        "x-unit": "seconds",
        "x-minimum": "1",
        "x-maximum": "31536000",
        "x-semantic": "minimum-tenure"
      }
    },
    "required": ["window"],
    "additionalProperties": false,
    "x-abi": [{ "name": "window", "type": "uint256" }]
  },
  "errors": [
    { "signature": "TenureNotConfigured()", "message": "Set a window above zero." },
    {
      "signature": "TenureTooLong(uint256)",
      "message": "Too long. The most this module allows is {0}.",
      "x-unit": "seconds"
    }
  ]
}

settings is a JSON Schema 2020-12 document a form library can take as is, plus these conventions:

KeyMeaning
x-abiThe values in ABI-encoding order, as viem AbiParameters. abi.encode of them is the slot's settings
x-optionalThe slot may leave settings empty
x-semanticA behaviour a client may recognise, such as "minimum-tenure"
x-unit, x-minimum, x-maximumWhat a number counts, and its bounds
x-enum-labelsLabels for an enumeration, in value order
x-formatHow a value is typed and read. "bytes32-string" is text packed into 32 bytes

errors, beside settings, lists how validateSettings may revert: the error's signature, a sentence to show, and the unit its arguments are in. {0}, {1} in the message are the error's arguments. A revert carries only a selector, so this is what lets a client name and word a refusal from a module it has never seen.

Every value is a string, because JSON numbers lose precision above 2^53; bounds travel as x-minimum / x-maximum strings. They are advice — validateSettings is the authority. A module that does not implement metadata(), or answers with something that is not JSON, is simply undescribed; fall back to its scopes.

MinimumTenureModule

One deployment per chain; the window a slot enforces is its own settings, a number of seconds.

function tenureOf(bytes settings) external pure returns (uint256); // settings = abi.encode(uint256 seconds)
function requiredDeposit(uint256 price, uint256 taxRateBps, uint256 window)
    external pure returns (uint256);
function reentryAllowedAt(address slot, address account) external view returns (uint256);
 
uint256 public constant MAX_TENURE = 365 days;
uint256 public constant BUYOUT_PREMIUM_BPS = 100_000; // 10x

Scopes: beforeBuy, beforeSelfAssess, afterRelease, afterLiquidate. No fee.

Entry is fundedevery buy must post enough deposit to cover a whole window, at the higher of the old and new price
No price cutinside the window, the occupant cannot lower their price
Buyouts cost a premiuminside the window, a buyer must declare at least 10x the occupant's price
No free renewalan occupant who releases or is liquidated cannot retake that slot for one window
ErrorMeaning
TenureNotConfiguredsettings is zero — a window was never set
TenureTooLong(maxTenure)above MAX_TENURE
TenureUnderfunded(required)the deposit does not cover the window
PriceCutDuringTenurea selfAssess lowering the price inside the window
TenureNotElapsed(allowedAt)the leaver's re-entry bar has not expired
BuyoutBelowPremium(required)a mid-window buy declaring less than 10x
NotTheSlotan after callback called by something other than the slot

AdLand

A sponsorship module: the occupant publishes a creative (Published), and it is cleared when the slot changes hands (Cleared). Its settings are abi.encode(AdConfig) — an optional minimum-tenure window, a moderation mode and a registry key — and empty settings configure none of them. It inherits the MinimumTenure rule, so one module covers both behaviours.

Scopes: afterBuy, afterRelease, afterLiquidate, plus beforeBuy and beforeSelfAssess when the config sets a window, so a slot without one grants no veto. No fee. Its creatives are indexed — see Indexer.

Slot-bound NFTs

SlotBoundNFTFactory deploys SlotBoundNFT collections, which mint a token and its slot together, and SlotBoundNFTWrapper, which puts an existing ERC-721 under common ownership. In both, the token belongs to whoever occupies its slot: ownership moves in afterBuy, afterRelease and afterLiquidate, and both declare afterCallbacksMustSucceed so the move cannot be dropped. The wrapper also declares beforeBuy, to refuse buys on a slot it has retired.