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".
| interface | id | covers |
|---|---|---|
IUtility | 0xe120614a | the six hooks |
IOccupancyPolicy | 0xd8a073cb | checkBuy, checkPriceUpdate |
IModuleMetadata | 0x51eed0df | name, 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
| Hook | Fires when |
|---|---|
onTransfer | occupancy moves, after state settles |
onPriceUpdate | occupant self-assesses |
onRelease | occupant leaves, or is liquidated |
onSettle | tax is charged |
onSettle
The economic hook. The other three report who holds the slot; this reports that money moved.
paidis what was actually taken. Use this for accounting.owedis what was due.owed - paidis 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.