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 onlybuy 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 occupiedliquidate() 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 stepmask names what to change:
| Bit | Constant | Changes | Needs |
|---|---|---|---|
1 | TERM_TAX_RATE | taxTerms.rateBps | mutableTax |
2 | TERM_RECIPIENT | taxTerms.recipient | mutableRecipient |
4 | TERM_MIN_RUNWAY | taxTerms.minRunwaySeconds | mutableTax |
8 | TERM_MODULE | the whole moduleTerms | mutableModule |
16 | TERM_SCOPES | queued by acceptScopes, never proposed | mutableModule |
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 copyWhen 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. RevertsFeeChangedif the module now says something else; a rise needsmutableRecipient.acceptScopes(expected)queues underTERM_SCOPESand lands at the next buy, only when the slot's module is mutable. RevertsScopesChangedif the module now says something else, andModuleChangeQueuedwhile 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 declaresOperatorSet 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.