Skip to content
0xSlots

Slot

The contract behind one position. Every mutating call settles accrued tax before anything else happens, so the numbers are correct whenever anyone looks — nothing runs on a timer.

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 a slot
function withdraw(uint256 amount) external;        // occupant only
function release() external;                       // occupant only, always allowed

buy separates who pays (msg.sender) from who holds (account), so a contract can acquire a slot on someone's behalf.

Selling into an offer

Slot.sell no longer exists. It took a buyer's signed order and seated them — a SECOND seating path, resetting the tenure like buy but running beforeSell instead of beforeBuy, so every hook author had two doors to police.

A consensual sale is now two calls the core already had, performed by the OfferBook inside the occupant's own transaction:

slot.selfAssess(price)                 // the occupant's price, restated
slot.buy(bidder, price, deposit, max)  // the ordinary market path

Atomic, so nothing can take the slot between them. The economics are unchanged — buy already refunds the outgoing occupant their deposit plus the price.

For the book to make the first call the occupant must make it their operator:

slot.setOperator(book, true);   // lapses with the tenure
book.acceptOffer(slot, id);     // occupant only

Permissionless operations

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

claim withdraws funds credited to an account when a direct payout failed (a refund to a contract that rejected it, say) — pull, not push.

Delegation

function setOperator(address operator, bool allowed) external;  // occupant only

An operator may selfAssess on the occupant's behalf — delegate price changes without handing over the slot.

Manager operations

Only if the slot was created with mutableTax or mutableHook, and only by the manager. Proposed terms apply after a delay (TERMS_DELAY, 1 day).

function proposeTerms(
    uint256 newTax,
    address newHook,       // address(0) detaches the hook
    bytes32 newHookData,   // that hook's configuration; must be 0 when it is
    bool    changeTax,
    bool    changeHook
) external;
 
function cancelTerms(bool cancelTax, bool cancelHook) external;

Tax and hook are proposed and cancelled independently, with a flag each. They may be governed by different parties behind one manager address; an all-or-nothing cancel would let whoever moves the hook wipe a queued tax change as a side effect. A change touching a mutable dimension you did not open reverts.

newHookData is not a third dimension — it rides under changeHook, because a hook and the configuration meant for it are one decision. The hook is asked to validate it now: one that rejects it is refused here rather than attached and vetoing every callback afterwards.

event TermsProposed(
    uint256 taxBps, address hook, bytes32 hookData, bool tax, bool hook_
);
event TermsApplied(uint256 taxBps, address hook, bytes32 hookData);
event TermsCancelled(bool tax, bool hook);

Reading state

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

Everything in one call — terms, the attached hook and its snapshotted HookFlags, live occupancy and financials, and any queued terms. Prefer it over assembling individual getters, which each re-read and re-settle.

struct SlotInfo {
    // terms
    address recipient; IERC20 currency; address manager;
    uint256 taxBps; uint256 minDepositSeconds;
    bool mutableTax; bool mutableHook;
    // extension
    address hook; bytes32 hookData; HookFlags hookFlags;
    // occupancy
    address occupant; uint256 price; uint256 deposit;
    uint64 occupiedSince; uint64 tenureId; uint64 lastSettled;
    // money, as of this block
    uint256 taxOwed; uint256 collectedTax;
    bool isVacant; bool isInsolvent; uint256 secondsUntilLiquidation;
    // queued terms
    uint256 pendingTaxBps; address pendingHook; bytes32 pendingHookData;
    bool pendingHasTax; bool pendingHasHook;
    uint64 pendingProposedAt; bool hasRipeTerms;
}

Individual getters exist for the common fields:

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);

Events

The occupancy and money log. Sold and OrderCancelled went with sell — a sale now emits Bought on the slot and Filled on the book.

event Bought(/* buyer, account, price, deposit, … */);
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 Settled(uint256 owed, uint256 paid, uint256 depositLeft);
event TaxPaid(address indexed payer, uint256 owed, uint256 paid);
event TaxCollected(address indexed recipient, uint256 amount);
event OperatorSet(address indexed operator, bool allowed, uint64 indexed tenureId);
event Credited(address indexed account, uint256 amount);   // a push payment failed
event Claimed(address indexed account, uint256 amount);    // and was later pulled

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 — an indexer would show a previous occupant's bot as a co-signer on somebody else's asking price.

TaxPaid is the one to reduce over for per-address accounting, and it always fires — including when an after hook call fails. A failed hook emits HookCallFailed(hook, selector) and is otherwise swallowed; a hook that reverts its own attachment check is dropped with HookDetached(hook). Neither can block the operation, which is the whole point of the after side.