Skip to content
0xSlots

Slot

The contract behind one position, deployed by the SlotFactory as a beacon proxy. Every mutating call settles accrued tax before anything else happens, so the numbers are correct whenever anyone looks — nothing runs on a timer. Slot inherits OpenZeppelin's Multicall.

Occupant operations

function buy(
    address account,          // who gets SEATED — not necessarily msg.sender
    uint256 selfAssessedPrice,
    uint256 depositAmount,
    uint256 maxPayment        // ceiling on the total charged; 0 disables it
) external payable;
 
function selfAssess(uint256 newPrice) external;   // occupant or operator
function topUp(uint256 amount) external payable;   // anyone may fund an occupied slot
function withdraw(uint256 amount) external;        // occupant only
function release() external;                       // occupant only
function setOperator(address operator, bool allowed) external; // occupant only

buy separates who pays (msg.sender) from who holds (account), so a contract can acquire a slot on someone's behalf. It charges quoteBuy(account, depositAmount): the sitting occupant's price (zero when vacant), plus the deposit, plus any debt account owes this slot. Native slots require msg.value to equal it exactly.

selfAssess, withdraw and buy all require the deposit to cover minRunwaySeconds of tax at the price in question. An operator may only selfAssess, and the grant lapses when the tenure ends.

A consensual sale goes through the OfferBook, which calls selfAssess then buy inside the occupant's own transaction.

Permissionless operations

function collect() external;              // pay out collected tax: module fee, then recipient
function liquidate() external;            // evict an occupant whose deposit is empty
function claim(address account) external; // pay `account` a credited payout
function applyTerms() external;           // land ripe terms — occupant only while occupied

liquidate() has no bounty. The reward is the slot itself: a vacant slot costs only the taker's own deposit, so whoever wants it can evict and buy it in one multicall.

A payout that cannot be pushed (a contract that rejects it, say) is credited instead of reverting; claim pulls it later.

Manager operations

Only by the manager, and only for what the slot was created mutable for.

function proposeTerms(TaxTerms calldata taxTerms, ModuleTerms calldata moduleTerms, uint16 mask) external;
function cancelTerms(uint16 mask) external;
function acceptFee(ModuleFee calldata expected) external;  // applies now
function acceptScopes(uint16 expected) external;          // queues for the next buy
function setManager(address next) external;               // immediate, one step

mask names what to change:

BitConstantChangesNeeds
1TERM_TAX_RATEtaxTerms.rateBpsmutableTax
2TERM_RECIPIENTtaxTerms.recipientmutableRecipient
4TERM_MIN_RUNWAYtaxTerms.minRunwaySecondsmutableTax
8TERM_MODULEthe whole moduleTermsmutableModule
16TERM_SCOPESqueued by acceptScopes, never proposedmutableModule

Fields outside the mask are ignored. Proposals are validated now — a module is asked validateSettings, scopes and fee at proposal — and then queue: they ripen after TERMS_DELAY (one hour) and land at the next buy, or earlier through applyTerms. Tax terms and the module ripen on separate clocks, so one role's change never holds another's back. Proposing again overwrites the named terms, keeps the rest queued, and restarts the delay of the group it touched. cancelTerms drops whichever of mask is queued and leaves the rest.

Modules

function module() external view returns (address);
function moduleTerms() external view returns (ModuleTerms memory);
function mutableModule() external view returns (bool);
function scopes() external view returns (Scopes memory);   // the slot's accepted copy, unpacked
function fee() external view returns (ModuleFee memory);   // the slot's accepted copy

When the module declares something different, the manager accepts it, passing the value they reviewed. Each reverts NothingToAccept if nothing would change.

  • acceptFee(expected) applies at once; tax collected so far is paid out under the old fee first. Reverts FeeChanged if the module now says something else; a rise needs mutableRecipient.
  • acceptScopes(expected) queues under TERM_SCOPES and lands at the next buy, only when the slot's module is mutable. Reverts ScopesChanged if the module now says something else, and ModuleChangeQueued while a new module is queued.

To see whether there is anything to accept, ask SlotLens.moduleUpdate(slot). The SDK's moduleUpdate(slot) calls it.

Reading state

One getter per field (occupant, price, terms, scopes, fee, …), and the constants are public (MAX_PRICE, TERMS_DELAY, MODULE_CALLBACK_GAS_LIMIT, the TERM_* bits, …). For a whole slot in one call, or many slots, use SlotLens.

Beyond the fields:

function quoteBuy(address account, uint256 depositAmount) external view returns (uint256);
function minDepositForBuy(uint256 price) external view returns (uint256);  // uses ripe queued terms
function minDepositToHold(uint256 price) external view returns (uint256);  // terms in force
function hasRipeTerms() external view returns (bool);
function pending() external view returns (Pending memory); // stored as-is
function isOperator(address operator) external view returns (bool);
function debtOf(address account) external view returns (uint256);
function claimableOf(address account) external view returns (uint256);

Size a buy's deposit with minDepositForBuy: a buy applies ripe queued terms before its funding check, so the buyer funds the terms they are buying into.

Events

event Initialized(address indexed currency, address indexed manager, bool mutableTax,
                  bool mutableRecipient, bool mutableModule, TaxTerms taxTerms, ModuleTerms moduleTerms);
event Bought(address indexed buyer, address indexed from, uint256 price, uint256 deposit, uint256 paid);
event Released(address indexed occupant, uint256 refund);
event Liquidated(address indexed by, address indexed occupant);
event PriceSet(address indexed by, uint256 oldPrice, uint256 newPrice);
event Deposited(address indexed by, uint256 amount, uint256 total);
event Withdrawn(address indexed occupant, uint256 amount, uint256 left);
event OperatorSet(address indexed operator, bool allowed, uint64 indexed tenureId);
 
event Settled(uint256 owed, uint256 paid, uint256 depositLeft);
event TaxPaid(address indexed payer, uint256 owed, uint256 paid);
event TaxCollected(address indexed recipient, uint256 amount);
event ModuleFeePaid(address indexed module, address indexed recipient, uint256 amount);
event DebtRepaid(address indexed account, uint256 amount);
event Credited(address indexed account, uint256 amount);  // a push payment failed
event Claimed(address indexed account, uint256 amount);   // and was later pulled
 
event TermsProposed(TaxTerms taxTerms, ModuleTerms moduleTerms, uint16 mask);
event TermsCancelled(uint16 mask);
event TermsApplied(TaxTerms taxTerms, ModuleTerms moduleTerms, uint16 scopes, ModuleFee fee, uint16 mask);
event ManagerSet(address indexed previous, address indexed next);
event FeeAccepted(ModuleFee fee);
event ScopesAccepted(uint16 scopes);
 
event ModuleCallFailed(address indexed module, bytes4 selector); // an effect callback reverted, swallowed
event ModuleDropped(address indexed module);                     // a queued module could not be installed
event ScopesDropped(address indexed module, uint16 scopes);      // accepted scopes the module no longer declares

OperatorSet carries the tenureId it is scoped to: an approval dies with the tenure and there is no revocation event, so a log without it cannot be replayed into isOperator.

TaxPaid is the one to reduce over for per-address accounting, and it fires whether or not the module's afterSettle succeeds.