Occupancy policies
See Occupancy for why these exist and when to use each.
IOccupancyPolicy
interface IOccupancyPolicy is IModuleMetadata {
function checkBuy(OccupancyContext calldata ctx) external view;
function checkPriceUpdate(OccupancyContext calldata ctx) external view;
}Self-description — name, version, metadataURI — lives in
IModuleMetadata, shared with IUtility. The two describe
themselves identically even though they behave nothing alike, so policyURI() and
moduleURI() were unified into one metadataURI(). As with IUtility,
inheriting narrows this interface's ERC-165 id to its own two checks, and
SlotFactory.setPolicyVerified asserts both ids.
Both checks are view and return nothing. Revert to block; return to allow.
A policy answers yes or no and does nothing else — it can never move funds,
change the price, or redirect the buyer.
Fail-closed: a policy that reverts blocks the action, and its revert reason bubbles up to the caller.
OccupancyContext
Everything the policy needs, passed by value so it never has to call back into the slot.
struct OccupancyContext {
address slot; // the calling slot
address caller; // msg.sender on the slot
address account; // incoming occupant (buy) / current one (price update)
address occupant; // current occupant, address(0) if vacant
uint256 occupiedSince; // when the current occupancy began
uint256 taxPercentage; // bps per 30 days
uint256 currentPrice;
uint256 newPrice;
uint256 depositAmount;
}liquidate() and release() never call a policy. Insolvency always ends an
occupancy, and an occupant can always leave.
IPolicyFactory
Any contract can claim to enforce a rule. This is how you check.
interface IPolicyFactory {
function policyKind() external pure returns (string memory);
function verify(address policy) external view returns (bool);
}verify recomputes the policy's CREATE2 address from its own immutable terms
and compares. CREATE2 binds an address to the deployer, the init code and the
salt, so only the genuine policy for those exact terms can sit there. Reading
terms from an untrusted address is safe because the comparison — not the read —
is what is trusted.
verify returns false rather than reverting for anything that is not a match,
including EOAs and unrelated contracts. Clients loop over factories, and a
revert would abort the loop on the first miss.
for (factory of factoriesForChain(chainId))
if (await factory.verify(policy)) return format(await factory.policyKind())
Adding a policy kind is "deploy a factory, list its address" — no change to verification logic anywhere.
Turning terms into a sentence — "7 day minimum tenure" — deliberately stays off-chain. It is presentation: safe to get wrong, wants translating, and would otherwise bake English into an immutable contract.
Shipped policies
MinimumTenurePolicy
uint256 public immutable tenureSeconds;checkBuyrequires the buyer to escrow the whole window's tax up front, and blocks any buy-out beforeoccupiedSince + tenureSeconds.checkPriceUpdateblocks price cuts while protected, so an occupant cannot declare high and then drop once safe.
Deployed per duration at a CREATE2 address derived from tenureSeconds.
MinimumPricePolicy
IERC20 public immutable currency;
uint256 public immutable minPrice;Rejects any declared price below the floor, on both buy and self-assessment. Bound to a currency, so a floor for one token cannot be applied to a slot denominated in another.
QueueExclusivityPolicy
While the slot is vacant and the queue is non-empty, only the queue contract may buy. Occupied slots are untouched — forced sale is never suspended.
Paired with the SlotQueue peripheral, which holds FIFO bids and can be filled
permissionlessly.