Protocol
SlotFactory
The factory deploys slots via BeaconProxy (UUPS-upgradeable). Each slot gets a deterministic address based on keccak256(recipient, currency, config).
function createSlot(
address recipient,
IERC20 currency,
SlotConfig memory config,
SlotInitParams memory initParams
) external returns (address slot);
function createSlots(
address recipient,
IERC20 currency,
SlotConfig memory config,
SlotInitParams memory initParams,
uint256 count
) external returns (address[] memory slots)SlotConfig (immutable)
struct SlotConfig {
bool mutableTax; // Can the tax rate be changed?
bool mutableModule; // Can the module be changed?
address manager; // Who can propose config changes (address(0) = no one)
}SlotInitParams
struct SlotInitParams {
uint256 taxPercentage; // Tax rate in bps per month (100 = 1%)
address module; // Hook contract (address(0) = none)
uint256 liquidationBountyBps; // Bounty for liquidators in bps
uint256 minDepositSeconds; // Minimum deposit to cover (protocol min: 1 day)
}The factory also maintains a module registry — modules can be verified so users know they're safe to use.
Slot
Each slot is a standalone smart contract. No shared state between slots.
Core operations
/// Buy a slot (or take it from current occupant)
function buy(address account, uint256 depositAmount, uint256 selfAssessedPrice) external;
/// Leave the slot, get remaining deposit back
function release() external;
/// Change your self-assessed price
function selfAssess(uint256 newPrice) external;
/// Add to your deposit
function topUp(uint256 amount) external;
/// Withdraw excess deposit
function withdraw(uint256 amount) external;
/// Liquidate an insolvent slot (anyone can call, earns bounty)
function liquidate() external;
/// Send accumulated tax to the recipient (anyone can call)
function collect() external;Manager operations
If mutableTax or mutableModule is true, the manager can propose changes. Updates only apply on the next ownership transition — the current occupant's terms never change under them.
function proposeTaxUpdate(uint256 newPct) external;
function proposeModuleUpdate(address newModule) external;
function cancelPendingUpdates() external;
function setLiquidationBounty(uint256 newBps) external;Tax
Tax accrues linearly: taxOwed = price * taxPercentage * elapsed / (30 days * 10000)
The occupant maintains a deposit that covers future tax. When depleted, anyone can liquidate() and earn a bounty.
Roles
| Role | Revenue | Config | Slot state |
|---|---|---|---|
| Recipient | Receives tax + sale proceeds | No control | No control |
| Manager | No revenue | Proposes tax/module changes | No control |
| Occupant | Pays tax | No control | Sets price, manages deposit |
Modules
Modules are optional hook contracts attached to a slot. They implement ISlotsModule (which extends IERC165):
interface ISlotsModule is IERC165 {
// Identity
function name() external view returns (string memory);
function version() external view returns (string memory);
// Lifecycle hooks (called by the Slot contract)
function onTransfer(uint256 slotId, address from, address to) external;
function onPriceUpdate(uint256 slotId, uint256 oldPrice, uint256 newPrice) external;
function onRelease(uint256 slotId, address from) external;
// Fee configuration
function feeBps() external view returns (uint256);
function feeRecipient() external view returns (address);
// Metadata
function moduleURI() external view returns (string memory);
}Lifecycle Hooks
Called by the Slot contract during state transitions. msg.sender is always the slot contract.
| Hook | Triggered by | Typical use |
|---|---|---|
onTransfer(slotId, from, to) | buy() | Clear metadata, update access control |
onPriceUpdate(slotId, oldPrice, newPrice) | selfAssess() | React to price changes |
onRelease(slotId, from) | release(), liquidate() | Clean up slot state |
Fee Configuration
Modules can take a cut from collected tax. The fee is deducted when collect() is called on the slot.
| Function | Description |
|---|---|
feeBps() | Fee in basis points (e.g. 500 = 5%). Taken from collected tax before it reaches the recipient. Return 0 for no fee. |
feeRecipient() | Address that receives module fees — can be an EOA, multisig, Splits contract, etc. |
Metadata
| Function | Description |
|---|---|
moduleURI() | URI pointing to module metadata (e.g. ipfs://Qm... with JSON containing image, description). Can be empty. |
ERC-165
Modules must return true for both ISlotsModule.interfaceId and IERC165.interfaceId in supportsInterface(). The factory uses this to verify a contract is a valid module.
MetadataModule
The primary module. Lets the occupant attach a URI (e.g. IPFS) to the slot. Clears metadata on transfer and release.
// Set metadata for a slot (occupant only)
function updateMetadata(address slot, string calldata uri) external;
// Read metadata
function tokenURI(address slot) external view returns (string memory);FeedPostModule
Like MetadataModule but supports trusted routers for atomic buy+post flows. Used by The Feed.
// Direct post (occupant only)
function updateMetadata(address slot, string calldata uri) external;
// Post via trusted router (atomic buy+post)
function postFor(address account, address slot, string calldata uri) external;