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;
}| Bit | Scope | Bit | Scope | |
|---|---|---|---|---|
1 << 0 | beforeBuy | 1 << 5 | afterSettle | |
1 << 1 | beforeSelfAssess | 1 << 6 | afterCallbacksMustSucceed | |
1 << 2 | afterBuy | 1 << 7 | onInstall | |
1 << 3 | afterRelease | 1 << 8 | onUninstall | |
1 << 4 | afterLiquidate |
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:
| Key | Meaning |
|---|---|
x-abi | The values in ABI-encoding order, as viem AbiParameters. abi.encode of them is the slot's settings |
x-optional | The slot may leave settings empty |
x-semantic | A behaviour a client may recognise, such as "minimum-tenure" |
x-unit, x-minimum, x-maximum | What a number counts, and its bounds |
x-enum-labels | Labels for an enumeration, in value order |
x-format | How 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; // 10xScopes: beforeBuy, beforeSelfAssess, afterRelease, afterLiquidate. No fee.
| Entry is funded | every buy must post enough deposit to cover a whole window, at the higher of the old and new price |
| No price cut | inside the window, the occupant cannot lower their price |
| Buyouts cost a premium | inside the window, a buyer must declare at least 10x the occupant's price |
| No free renewal | an occupant who releases or is liquidated cannot retake that slot for one window |
| Error | Meaning |
|---|---|
TenureNotConfigured | settings is zero — a window was never set |
TenureTooLong(maxTenure) | above MAX_TENURE |
TenureUnderfunded(required) | the deposit does not cover the window |
PriceCutDuringTenure | a 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 |
NotTheSlot | an 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.