Skip to content

predict: add App-witness gated custom-owner PredictManager constructor#1010

Open
SeventhOdyssey71 wants to merge 1 commit into
MystenLabs:mainfrom
SeventhOdyssey71:feat/predict-manager-custom-owner
Open

predict: add App-witness gated custom-owner PredictManager constructor#1010
SeventhOdyssey71 wants to merge 1 commit into
MystenLabs:mainfrom
SeventhOdyssey71:feat/predict-manager-custom-owner

Conversation

@SeventhOdyssey71

Copy link
Copy Markdown

predict: add App-witness gated custom-owner PredictManager constructor

Summary

Adds a small App-witness authorization layer to predict::registry and a
new_with_custom_owner constructor to predict::predict_manager, mirroring
the patterns already in deepbook::registry and
deepbook::balance_manager. Net new public surface:

// predict::registry
public struct AppKey<phantom App: drop> has copy, drop, store {}
public fun authorize_app<App: drop>(registry: &mut Registry, _admin_cap: &AdminCap)
public fun deauthorize_app<App: drop>(registry: &mut Registry, _admin_cap: &AdminCap): bool
public fun assert_app_is_authorized<App: drop>(registry: &Registry)
public fun create_manager_for_custodian<App: drop>(
    registry: &mut Registry,
    owner: address,
    ctx: &mut TxContext,
): PredictManager

// predict::predict_manager
public(package) fun new_with_custom_owner(
    registry_uid: &mut UID,
    owner: address,
    ctx: &mut TxContext,
): PredictManager

No changes to any existing call site. The current predict_manager::new
becomes a thin wrapper that calls a shared new_internal with
ctx.sender() as the owner — behaviour-equivalent to today.

Motivation

Right now, the only way to create a PredictManager is via
registry::create_manager(...), which hardcodes the owner to
ctx.sender(). That works for end users, but it makes it impossible to
build composable protocols that need to escrow a Predict position
inside a contract-controlled custodian.

Concrete use cases this unblocks:

  • Margin / lending against options. A vault that lends USDC against
    an open Predict position needs to be able to mint the position into a
    manager only the vault controls. Today the manager's owner is always
    the borrower, so the borrower can withdraw the collateral out from
    under the loan.

  • Vol Yield / structured products. A short-vol vault that sells
    straddles on Predict and routes the strategy programmatically needs a
    contract-owned manager, not a user-owned one.

  • Range-ladder / PLP-hedge vaults. Same shape — the vault is the
    position owner.

The deepbook side of the monorepo already solves this exact problem
for BalanceManager via new_with_custom_owner +
new_with_custom_owner_caps<App>, with admin-gated App witness
authorization on the Registry. This PR brings the same pattern to
predict_manager, which is the analogous custody object for the Predict
protocol.

Why this is the minimal change

Every new line copies a precedent that already exists in
packages/deepbook:

Added in this PR Precedent (already in packages/deepbook)
AppKey<phantom App: drop> deepbook::registry::AppKey
authorize_app<App: drop> deepbook::registry::authorize_app
deauthorize_app<App: drop> deepbook::registry::deauthorize_app
assert_app_is_authorized<App: drop> deepbook::registry::assert_app_is_authorized
new_with_custom_owner(registry_uid, owner, ctx) balance_manager::new_with_custom_owner(owner, ctx)
create_manager_for_custodian<App> entry balance_manager::new_with_custom_owner_caps<App>

Reviewers can verify by diffing against the corresponding deepbook code
side-by-side.

Security

The new entry point is double-gated:

  1. Admin gate. Only the holder of AdminCap can call
    authorize_app<App>(...). So an unprivileged caller cannot grant
    itself custodian access.
  2. Type-system gate. App is a phantom witness type from the
    custodian's own package. The Move type system enforces that only the
    package that defined App can reference it as the type parameter to
    create_manager_for_custodian<App>. So even if two distinct
    protocols both authorize a witness called MyApp, they are different
    types
    and the auth is scoped per type.

The created manager's owner is whatever address the custodian passes —
typically the manager's own derived address, or the address of the
custodian's outer shared object. Same shape DeepBook Margin already uses
to lock its own BalanceManager.

Test plan

  • Move tests added under packages/predict/tests/registry/custodian_tests.move:
    • authorize_then_create_for_custodian_succeeds — happy path; manager
      owner is the custodian address, not the tx sender.
    • unauthorized_app_abortscreate_manager_for_custodian<UnauthorizedApp>
      aborts with EAppNotAuthorized.
    • deauthorize_removes_accessdeauthorize_app returns true and
      the App key is removed.
    • create_for_custodian_with_owner_eq_sender_still_works — the
      custom-owner path is general; owner == sender works the same as
      create_manager.
  • Existing test suite unaffected — new() signature and behaviour
    unchanged.

Out of scope

  • No changes to predict::mint, predict::redeem, or related entry
    points. Existing flows continue to work as today.
  • No support for multiple managers per owner. PredictManagerKey still
    hardcodes the secondary index to 0. If/when multi-manager-per-owner
    becomes a requirement, that's a separate, additive change.

How to verify locally

cd packages/predict
sui move build
sui move test custodian_tests

Downstream

Once this lands on main and rolls into the next testnet/mainnet
deployment, the following projects can ship:

  • predict_backed_margin — on-chain margin loans against options
  • vol_yield — variance risk premium tokenization
  • range-ladder / PLP+hedge vaults from the
    hackathon idea bank

Mirrors the patterns already in deepbook::registry +
deepbook::balance_manager. Lets external custodian protocols (margin
loans, structured vaults, range ladders, PLP+hedge) mint a
PredictManager whose owner is a contract-derived address rather than
the transaction sender.

predict_manager.move
- new() is now a thin wrapper around new_internal that hardcodes
  ctx.sender() as the owner — behaviour-equivalent to before.
- new_with_custom_owner(registry_uid, owner, ctx) is the new
  package-internal constructor. Wraps the inner BalanceManager via
  balance_manager::new_with_custom_owner(owner, ctx), so when `owner`
  is a contract-derived address the BalanceManager is effectively
  locked to that contract.

registry.move
- AppKey<phantom App: drop> dynamic field marker.
- authorize_app<App>(registry, admin_cap) — admin-gated.
- deauthorize_app<App>(registry, admin_cap): bool.
- assert_app_is_authorized<App>(registry).
- create_manager_for_custodian<App>(registry, owner, ctx) public entry.
  Double-gated: admin-set AppKey authorization + type-system witness.
  Calls predict_manager::new_with_custom_owner.

tests/registry/custodian_tests.move
- happy path: manager owner is the custodian address, not the sender.
- unauthorized App aborts with EAppNotAuthorized.
- deauthorize_app removes access, returns true.
- owner == sender path still works (custom-owner is general).

No changes to existing call sites. Existing test suite unaffected.

Use case unblocked: composable custodian protocols on top of Predict.
Today there is no way for an external contract to escrow a Predict
position — the manager's owner is always ctx.sender(), and the caps
are sealed inside. DeepBook Margin solves this exact problem for
BalanceManager via new_with_custom_owner_caps<App>; this PR brings the
same pattern to PredictManager.

See .PR-BODY.md (not committed) for full PR description with motivation
+ test plan + security analysis.
@SeventhOdyssey71

Copy link
Copy Markdown
Author

@amnn @patrickkuo @aschran

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant