Skip to main content

Registry

deepbook_predict::registry::Registry is the shared object that holds the market catalog and the cadence configuration for every registered underlying. It also holds the allowlists that decide which PauseCap, which MarketLifecycleCap, and which PoolValuationCap still carry authority.

Publishing the Predict package creates Registry and ProtocolConfig together and transfers the single AdminCap to the publisher at that moment. The registry object ID for each deployment is on Contract Information, and getConfig(network).objects.registry in the SDK resolves the same ID. The Move snippets on this page pin the Mainnet source commit, which serves both deployments.

Most application integrations call only the read functions here. The rest are operator and governance surfaces. None of them is an entry fun: every function on this page is a public fun invoked as a moveCall. The capability-minting functions and generate_pool_valuation_proof return values that a later command in the same transaction must consume, because Sui rejects an unused return value only when its type lacks drop. The 2 creation functions each return an ID, which has drop, so a transaction can leave it unused.

Registry state

The registry holds the market catalog inline rather than in a separate object:

The market_manager field is a deepbook_predict::market_manager::MarketManager, a store value embedded in the registry. It carries 2 tables: the per-underlying cadence configuration, and the map from an underlying and expiry to a market ID. MarketManager is not a separate shared object, so there is no second ID to pass. The 3 VecSet<ID> fields are the capability allowlists.

Market lookup

Each market is its own shared ExpiryMarket object, addressed by the underlying and the expiry:

expiry_market_id returns Option<ID>, which is empty when no market exists for that pair, including when the underlying is not registered at all. The expiry is a Unix millisecond timestamp on the cadence grid, so compute the exact expiry before looking a market up, or list the active markets with client.predict.read.markets() in the SDK, which reads them over simulated transactions with no server in the path.

The table key behind that lookup, market_manager::MarketKey, has no accessors and no exported constructor. Treat it as internal, and address markets through expiry_market_id or through read.markets().

Cadence configuration

A cadence is one market schedule for one underlying. Cadence IDs never change:

Cadence IDNamePeriod
01m1 minute
15m5 minutes
21h1 hour
31d1 day
41w1 week
51mo30 days

A cadence ID above 5 aborts with EInvalidCadence. Read one cadence or all 6:

cadence_configs always returns one entry per cadence ID, in ID order, so index 3 is always the 1d cadence. A disabled cadence comes back with every term set to zero, and registering a new underlying starts it with all 6 cadences disabled. Both reads abort with EUnderlyingNotRegistered when the underlying has no row.

Both reads return CadenceConfig by value, and it carries the 5 terms a market snapshots at creation:

Read the terms with the accessors rather than by field:

cadence_enabled reads no stored flag: a cadence counts as enabled exactly when its window size is greater than zero. Both deployments register one underlying, BTC, and enable the same 2 cadences with the same terms:

CadenceEnabledtick_sizeadmission_tick_sizemax_expiry_allocationinitial_expiry_cashwindow_size
0 1mYes10000000 (0.01 USD)1000000000 (1 USD)10000000000 (10,000 USDC)2000000000 (2,000 USDC)2
1 5mYes10000000 (0.01 USD)1000000000 (1 USD)10000000000 (10,000 USDC)2000000000 (2,000 USDC)2
2 1h, 3 1d, 4 1w, 5 1moNo00000

A window size of 2 means the registry can hold 2 open markets per enabled cadence at once, 4 in total. These are mutable protocol state: CadenceConfigUpdated fired once per cadence at deployment on each network, and a later update changes the grid for markets created afterward. Read them onchain before sizing a strike. The same table with the underlying and oracle IDs is on Contract Information.

Capabilities

Privileged functions sit behind 4 capability types, and each one is a distinct owned object:

CapabilityModuleGates
AdminCapdeepbook_predict::adminProtocol configuration, underlying and cadence configuration, per-market mint pause and unpause, and the minting and revocation of the other 3 capabilities
PauseCapdeepbook_predict::pause_capThe 3 one-way emergency stops
MarketLifecycleCapdeepbook_predict::market_lifecycle_capMarket creation
PoolValuationCapdeepbook_predict::pool_valuation_capStarting a pool flush, through generate_pool_valuation_proof

Publication mints AdminCap once, and it has no rotation or revocation path. An admin mints the other 3 on demand and revokes them by ID, so their authority is a row in the registry rather than possession of the object alone. Each has an id accessor and a destroy function the holder can call on a capability it no longer needs. Market creation and flush authority are deliberately separate objects, so an operator can hold one without the other.

Mint and revoke a pause capability

Pause capabilities are deliberately not version-gated, so an emergency stop still works when the running package version is below the configured watermark:

mint_pause_cap returns the new PauseCap by value, so the transaction must transfer it to a holder in a later command. Revocation removes the ID from allowed_pause_caps, after which every pause path using that object aborts with EPauseCapNotValid, even though the object still exists. A holder can also destroy their own capability with pause_cap::destroy.

Mint and revoke a lifecycle capability

Lifecycle capabilities are version-gated, unlike pause capabilities:

Revoking an ID that is not in allowed_lifecycle_caps aborts with ELifecycleCapNotFound, and using a revoked capability aborts with ELifecycleCapNotValid. create_and_share_expiry_market checks a lifecycle capability by reference and produces no proof object.

Mint and revoke a pool valuation capability

Pool valuation capabilities follow the same version-gated mint and ungated revoke pattern. mint_pool_valuation_cap takes _admin_cap before config, the reverse of mint_lifecycle_cap:

Revoking an ID that is not in allowed_pool_valuation_caps aborts with EPoolValuationCapNotFound, and generate_pool_valuation_proof aborts with EPoolValuationCapNotValid for a capability that is not allowlisted.

generate_pool_valuation_proof converts an allowlisted capability into a plp::PoolValuationProof. That proof has no abilities, so no transaction can store, copy, or drop it: the same transaction must consume it, and its only consumer is plp::start_pool_valuation. Checking the allowlist at proof time lets a revoked capability fail before a flush starts. The transaction that starts a flush also has to snapshot every active market and seal the snapshot, while the later valuation and finish stages are permissionless and span as many transactions as they need. See Vault for the full sequence.

Pause and freeze

The registry has 3 emergency-stop functions, and each validates the capability ID against the registry allowlist before it acts:

Each takes no boolean, so each is one-way:

  • pause_trading_pause_cap: Sets trading_paused on ProtocolConfig. Flows guarded by the trading check then abort with ETradingPaused.
  • freeze_protocol_pause_cap: Engages the protocol freeze. Version-gated flows then abort with EProtocolFrozen.
  • pause_expiry_market_mint_pause_cap: Pauses minting on one market. Minting on that market aborts with EMintPaused, while redemption and settlement stay open.

Reversal is an AdminCap surface, not a PauseCap one. protocol_config::set_trading_paused, protocol_config::set_frozen, and expiry_market::set_mint_paused each take a boolean and can move the flag in either direction. set_frozen is intentionally not version-gated, so an admin can still unfreeze a frozen protocol.

Underlying and cadence administration

An admin must register an underlying before any market on it can exist:

Registering the same underlying twice aborts with EUnderlyingAlreadyRegistered. Registration creates the row with all 6 cadences disabled, so a follow-up set_template_cadence_config call per cadence actually turns a schedule on.

set_template_cadence_config validates the whole cadence as a unit. All 5 terms at zero disable the cadence. Otherwise every term must be nonzero, and the call aborts with EInvalidCadenceConfig when the terms mix zero and nonzero values, when the admission tick size is smaller than the tick size or not a whole multiple of it, when initial_expiry_cash is below the expiry_cash_floor of 1,000 USDC, or when it exceeds max_expiry_allocation. Both tick sizes must also pass the market tick bounds in the configuration constants: a tick that is not a positive multiple of 10000 aborts with EInvalidMarketTickSize, and one too large to multiply by the top tick aborts with EMarketTickSizeTooLarge. A window size above the maximum of 10 aborts with EInvalidCadenceWindowSize. A successful update emits CadenceConfigUpdated.

Changing a cadence does not touch markets that already exist. Each market snapshots its terms at creation, so the grid an open market admits is the grid from its creation.

Market creation

Market creation is the only lifecycle-capability surface in the registry, and its signature matches the previous deployment:

The call passes 3 gates before it does anything: the package version must be at or above the watermark, the lifecycle capability must be on the allowlist, and the trading_paused flag must be false. A pool flush in flight does not block it: creation moves no cash, the new market is not in the flush's frozen expected set, and it joins the next flush's snapshot. It then picks the next deployable expiry inside the cadence's rolling window and aborts when no slot qualifies:

  • ECadenceDisabled: The cadence window size is zero.
  • ECadenceWindowExceeded: Every slot inside the rolling window is already deployed.
  • EMarketAlreadyCreated: A market already exists for that underlying and expiry.
  • EInvalidDeploymentExpiry: The expiry does not align with the cadence grid, or does not pass the last deployed watermark.
  • EPythFeedNotBoundToUnderlying and EBlockScholesStoresNotBoundToUnderlying: The underlying has no canonical oracle bindings in the Propbook registry.

The function takes &OracleRegistry but no feed objects, so market creation reads no live price. The absolute tick domain and the cadence terms, fixed in advance, decide strike admission entirely. The call sets the market's reference tick source timestamp to the expiry minus one cadence period, and expiry_market::set_reference_tick records the reference tick later from the exact Pyth observation at that timestamp.

The cadence carries 2 values into the new market: admission_tick_size becomes the market's admission grid, and max_expiry_allocation becomes both the vault's allocation cap for that expiry and the scale of the market's inventory-impact curve. The call returns the new market ID and emits MarketCreated.

A new market starts with zero cash and cannot mint until the permissionless plp::rebalance_expiry_cash funds it. The pool also caps live pre-expiry markets at 24 and aborts with EMaxLiveExpiryMarketsExceeded beyond that.

Builder codes

Builder-code creation is the only permissionless function in the registry that changes state. The reads on this page, expiry_market_id, cadence_config, and cadence_configs, are permissionless too, but they mutate nothing:

Anyone can call it. The resulting BuilderCode is a shared object derived from the registry UID and keyed by the caller address plus the index you pass, so its address is deterministic and one address can hold many codes. The call returns the code ID and emits BuilderCodeCreated.

An account attaches a code with predict_account::set_builder_code and detaches it with predict_account::unset_builder_code. Builder fees accrue to the code address through the funds accumulator, and only the owner can call builder_code::claim_all_builder_fees, which returns a Coin<USDC>.

Protocol configuration

ProtocolConfig is the registry's sibling shared object. Its AdminCap setters cover fees, pricing freshness, the strike-exposure template, the exponentially weighted moving average (EWMA) penalty, the liquidity provider (LP) request policy, and 2 timing windows that other pages depend on:

SetterFieldBoundsDeployedBlocked during a flushAbort
set_no_trade_window_msno_trade_window_ms0 to 15000 ms, where 0 disables the window2000Noconfig_constants::EInvalidNoTradeWindowMs (24)
set_max_valuation_window_msmax_valuation_window_ms1 minute to 4 hours300000Yes, with EValuationInProgressconfig_constants::EInvalidMaxValuationWindowMs (23)

no_trade_window_ms is the interval before each expiry in which live quotes, mints, and live redeems abort with ETradeWindowClosed; see Predict. A flush deliberately does not block it, matching set_trading_paused, so a stalled flush cannot trap a safety control. A change emits NoTradeWindowUpdated. max_valuation_window_ms is the deadline after which an in-flight flush can no longer finish and the operator must restart it; see Vault. The setter aborts while a flush is in flight, so no one can move a started flush's deadline under it.

They come with 2 permissionless reads: valuation_in_progress(config): bool reports whether a flush is in flight, and no_trade_window_ms(config): u64 returns the live window. Both deployments hold the same values for every ProtocolConfig field; the full set is on Contract Information.

Errors

deepbook_predict::registry raises 5 capability errors:

CodeNameRaised when
0EPauseCapNotValidThe pause capability ID is not in allowed_pause_caps
1ELifecycleCapNotValidThe lifecycle capability ID is not in allowed_lifecycle_caps
2ELifecycleCapNotFoundRevoking a lifecycle capability ID that the allowlist never held
3EPoolValuationCapNotValidgenerate_pool_valuation_proof with a capability ID that is not in allowed_pool_valuation_caps
4EPoolValuationCapNotFoundRevoking a pool valuation capability ID that the allowlist never held

deepbook_predict::market_manager raises the catalog and cadence errors:

CodeNameRaised when
0EUnderlyingNotRegisteredNo row exists for the underlying
1EUnderlyingAlreadyRegisteredregister_underlying called twice for one underlying
2ECadenceDisabledDeploying on a cadence whose window size is zero
3EMarketAlreadyCreatedA market already exists for that underlying and expiry
4EInvalidCadenceCadence ID above 5
5ECadenceWindowExceededNo deployable slot inside the rolling window
6EInvalidDeploymentExpiryExpiry not grid-aligned, or not past the watermark
7EInvalidCadenceConfigMixed zero and nonzero terms, admission tick smaller than or not a multiple of the tick, or initial expiry cash below 1,000 USDC or above the allocation cap
8EPythFeedNotBoundToUnderlyingNo canonical Pyth binding for the underlying
9EBlockScholesStoresNotBoundToUnderlyingNo Block Scholes store pair for the underlying

deepbook_predict::protocol_config raises 8 gate errors. The registry's own functions reach only the first and the 2 version gates; the rest surface through the trade and flush calls that read the same object:

CodeNameRaised when
0ETradingPausedA trading-gated flow ran while trading_paused
1EValuationInProgressA flow the flush mark reads, such as an LP cancel, a sponsorship, or a mark-affecting setter, ran while a flush was in flight
2EValuationNotInProgressA flush stage ran with no flush in flight
3EPackageVersionDisabledThe running package version is below the watermark
4EVersionWatermarkNotAdvancedbump_version_watermark did not strictly increase the watermark
5EProtocolFrozenA version-gated flow ran while frozen
6ESnapshotInProgressA trade, a settled redeem, or a second start_pool_valuation landed inside a flush's snapshot transaction
7ETradeWindowClosedA live quote, mint, or live redeem ran inside the no-trade window

Setter bounds failures come from deepbook_predict::config_constants, codes 0 through 24, including EMarketTickSizeTooLarge (16), EInvalidCadenceWindowSize (15), EInvalidMaxValuationWindowMs (23), and EInvalidNoTradeWindowMs (24).

Events

Registry activity surfaces through deepbook_predict::config_events:

MarketCreated carries the full snapshot a market took at creation, so an indexer can build the market catalog from events alone. CadenceConfigUpdated carries the registry ID alongside the new terms, so an indexer can replay cadence history without re-reading the object; its layout matches the previous deployment. NoTradeWindowUpdated carries the new window and the onchain timestamp of the change.