Skip to content
0xSlots

Utility modules

See Utility modules for what these are for.

IUtility

interface IUtility is IModuleMetadata {
    function onTransfer(uint256 slotId, address from, address to) external;
    function onPriceUpdate(uint256 slotId, uint256 oldPrice, uint256 newPrice) external;
    function onRelease(uint256 slotId, address from) external;
    function onSettle(
        uint256 slotId,
        address occupant,
        uint256 owed,
        uint256 paid
    ) external;
 
    function feeBps() external view returns (uint256);
    function feeRecipient() external view returns (address);
}

slotId is always 0 — one slot is one contract, so the calling slot is identified by msg.sender. Key your state off that.

ISlotsModule is the former name, kept so existing utilities keep compiling. It is declared in full rather than as interface ISlotsModule is IUtility {} — an ERC-165 id is the XOR of an interface's own selectors and ignores inherited ones, so the empty-inheriting version would compute 0x00000000 and fail verification despite every function being correct. New code should implement IUtility; the two are ABI-identical.

IModuleMetadata

How both of a slot's pluggable contracts describe themselves. moduleURI() on utilities and policyURI() on policies returned the same kind of string for the same reason, so they are now one function on one shared interface:

interface IModuleMetadata is IERC165 {
    function name() external view returns (string memory);
    function version() external view returns (string memory);
    function metadataURI() external view returns (string memory);
}

This is metadata, not protocol. Nothing in Slot branches on it — the URI is read once at verification time and surfaced to indexers and UIs. A utility whose metadataURI() reverts is still a working utility; Slot._utilityURI swallows the failure.

ERC-165 ids

Moving name / version / metadataURI to a parent narrows each child's id, and the narrowing is the point: IUtility's id now means exactly "implements the utility hooks" and IModuleMetadata's means "describes itself".

interfaceidcovers
IUtility0xe120614athe six hooks
IOccupancyPolicy0xd8a073cbcheckBuy, checkPriceUpdate
IModuleMetadata0x51eed0dfname, version, metadataURI

SlotFactory.setUtilityVerified and setPolicyVerified assert both ids. Checking one alone would verify a contract that cannot describe itself — and the event they emit immediately reads all three fields. Anything hardcoding the old single id must move to checking both, or it will call a contract verified that the chain then rejects.

Event signatures are unchanged — parameter names are not part of a topic0 — so log decoding keeps working. Only the field label moved, plus the function selector on the contracts themselves.

Hooks

HookFires when
onTransferoccupancy moves, after state settles
onPriceUpdateoccupant self-assesses
onReleaseoccupant leaves, or is liquidated
onSettletax is charged

onSettle

The economic hook. The other three report who holds the slot; this reports that money moved.

  • paid is what was actually taken. Use this for accounting.
  • owed is what was due. owed - paid is non-zero exactly when the occupant has run out of deposit, which is a useful distress signal.

They diverge because a charge is capped by the remaining deposit. Reconstructing contributions from price × time computes owed, and someone can exploit that deliberately: declare a huge price with a tiny deposit, accrue enormous owed, pay almost nothing.

Unlike the other hooks, onSettle fires mid-transaction — from inside the settlement that every mutating call begins with. The slot is in its pre-operation state; during a buy, occupant() still returns the outgoing occupant. Reentry into the same slot is blocked, but treat anything you read as in flux.

Failure is silent

Module calls are gas-capped and their failures are swallowed. A module that reverts emits ModuleCallFailed and the slot proceeds.

So a module must never be the source of truth for anything financial — if a call is dropped, nothing tells it. Reduce over the TaxPaid event instead, which always fires regardless of what the module did.

Fees

function feeBps() external view returns (uint256);       // e.g. 500 = 5%
function feeRecipient() external view returns (address);

Skimmed from tax when it is collected; the remainder goes to the slot's recipient. Both are read from the module at collection time, so a module with no valid fee recipient simply takes nothing.

Shipped modules

MetadataModule

Stores a URI and structured metadata per slot, set by the occupant and cleared on release. The general-purpose module for "holding this slot means this content appears here" — ads, listings, profiles.

Takes no fee.

FeedPostModule

Grants the occupant the right to post into a feed. Takes no fee.