Skip to content
0xSlots

Slot

One slot, one contract. Deployed as a BeaconProxy by the SlotFactory.

Occupant operations

function buy(
    address account,
    uint256 depositAmount,
    uint256 selfAssessedPrice
) external;

Takes the slot. Pays the current occupant their declared price, plus depositAmount into escrow. account is who becomes occupant — it need not be msg.sender, so you can buy on someone's behalf.

Vacant slots cost only the deposit; there is no incumbent to pay.

function selfAssess(uint256 newPrice) external;   // occupant or operator
function topUp(uint256 amount) external;          // anyone may fund a slot
function withdraw(uint256 amount) external;       // occupant only
function release() external;                      // occupant only, always allowed

topUp is deliberately open: anyone can keep a slot solvent, including the recipient who benefits from it staying occupied.

withdraw takes back surplus deposit, subject to minDepositSeconds. release returns the remaining deposit and vacates.

Permissionless operations

function collect() external;     // push accrued tax to the recipient
function liquidate() external;   // remove an insolvent occupant, earn the bounty
function claim(address account) external;

Anyone may call these. liquidate requires the deposit to be fully exhausted; the bounty is liquidationBountyBps of collected tax, not of anyone's deposit.

claim is the escape hatch for a refund that could not be pushed — a blocklisting token, a contract that reverts on receipt. Rather than let that brick the slot, the amount is credited to withdrawableOf and claimed later.

Delegation

function setOperator(address operator, bool approved) external;
mapping(address => mapping(address => bool)) public isOperator;

Lets another address manage your price without holding the slot. Keyed by occupant, so approvals survive leaving and re-entering.

Manager operations

Only available for whatever was declared mutable at creation.

function proposeTaxUpdate(uint256 newPct) external;
function proposeUtilityUpdate(address newUtility) external;
function proposePolicyUpdate(address newPolicy) external;
function setLiquidationBounty(uint256 newBps) external;

Changes are proposed, not applied. They take effect at the next occupancy change, so the terms cannot shift under someone mid-tenancy.

proposeModuleUpdate remains as a deprecated alias for proposeUtilityUpdate.

Pending updates, one dimension at a time

A slot has always held up to three queued changes — tax, utility, occupancy policy — but they could once only be cancelled as a set, and the log could not say which one had moved. Each is now addressed independently:

enum UpdateKind { Tax, Utility, Policy }   // 0, 1, 2
 
function cancelPendingUpdate(UpdateKind kind) external;  // one dimension
function cancelPendingUpdates() external;                // all three
function pendingUpdateOf(UpdateKind kind)
    external view returns (bool isSet, bytes32 value, uint64 proposedAt);
 
function taxProposedAt() external view returns (uint64);
function utilityProposedAt() external view returns (uint64);
function policyProposedAt() external view returns (uint64);

pendingUpdateOf reads one kind uniformly across both storage structs — tax and utility live in PendingUpdate, policy in PendingPolicyUpdate.

Three events carry the same shape, where value is the proposed value widened to 32 bytes — the raw integer for Tax, the left-padded address for Utility and Policy:

event UpdateProposed(UpdateKind indexed kind, bytes32 value, uint64 proposedAt);
event UpdateCancelled(UpdateKind indexed kind);
event UpdateApplied(UpdateKind indexed kind, bytes32 value);

These are additions, not replacements. TaxUpdateProposed, ModuleUpdateProposed, PolicyUpdateProposed, PendingUpdateCancelled, PendingUpdateApplied and PolicyUpdateApplied all still fire — changing an existing event's signature changes its topic0 and splits historical indexing across two shapes.

What the new ones add is what the old ones structurally cannot express. PendingUpdateApplied carries both tax and utility on every apply, filling the unchanged one in from current state, so a reader sees a utility "change" to the value it already had. UpdateApplied fires only for what actually moved.

Reading state

function getSlotInfo() external view returns (SlotInfo memory);

Everything in one call — identity, live financials, module metadata, pending updates, occupancy policy. Prefer it over assembling individual getters, which costs more round trips and can tear across a state change.

Individual getters exist too:

function occupant() external view returns (address);
function price() external view returns (uint256);
function deposit() external view returns (uint256);
function taxOwed() external view returns (uint256);
function isInsolvent() external view returns (bool);
function isVacant() external view returns (bool);

Tax

Tax accrues per second at taxPercentage basis points per 30 days:

owed = price × taxPercentage × elapsed / (30 days × 10_000)

It is charged against the deposit at the start of every mutating call, so state is always settled before anything else happens. Nothing runs on a timer.

A charge is capped by the remaining deposit. An occupant who has run dry pays less than they owe and becomes liquidatable — which is why accounting must use amounts actually paid rather than amounts theoretically owed.

Events

EventFires on
Boughtoccupancy transfer
Releasedoccupant leaves
Liquidatedinsolvent occupant removed
PriceUpdatedself-assessment
Deposited / Withdrawndeposit changes
Settledtax charged — amounts only
TaxPaidtax charged — with the payer
TaxCollectedtax pushed to the recipient

TaxPaid is the one to reduce over for per-address accounting. Settled carries the same amounts but not who paid them, so it cannot be attributed. TaxPaid always fires, including when a module hook fails.