Skip to content
Draft
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3938ac0
predict: single version watermark on ProtocolConfig
0xaslan Jun 16, 2026
e5a70ce
Add reusable account package scaffold
0xaslan Jun 17, 2026
3c12479
Move account creation into registry
0xaslan Jun 17, 2026
6ab37da
Refine account authority model
0xaslan Jun 17, 2026
359fa86
Refine account custody model
0xaslan Jun 17, 2026
49193e0
Simplify Predict account data access
0xaslan Jun 17, 2026
8908a64
Rewire expiry market to accounts
0xaslan Jun 17, 2026
5ad98a6
Make Predict trade flows account-reference based
0xaslan Jun 17, 2026
e6e0ab4
Rewire PLP flows to accounts
0xaslan Jun 17, 2026
4cca296
Remove Predict manager surface
0xaslan Jun 17, 2026
6b93246
Derive canonical account receive identity
0xaslan Jun 17, 2026
34e0567
Clarify account identity API
0xaslan Jun 17, 2026
b11b579
Use explicit address balance claims
0xaslan Jun 17, 2026
21258c8
Restore account accumulator settlement
0xaslan Jun 17, 2026
bb92473
Use account auth hot potato for predict flows
0xaslan Jun 18, 2026
d5fbd5b
Add account accumulator test coverage status
0xaslan Jun 18, 2026
2dda9e6
Pin Predict tests to nightly framework for accumulator root construction
0xaslan Jun 18, 2026
52d9d4d
Rewire flow_test_helpers to the account custody model (WIP)
0xaslan Jun 18, 2026
2de30e1
Rewire Predict flow tests to the account custody model
0xaslan Jun 18, 2026
eb28caf
Rewire standalone Predict tests off the deleted PredictManager
0xaslan Jun 18, 2026
39d61fc
Update Predict indexer for the account rewire
0xaslan Jun 18, 2026
65184cb
Update stale 'manager' comments to 'account' in rewired flow tests
0xaslan Jun 18, 2026
10f5716
Update registry_guard_tests stale comments + add builder-code ENotOwn…
0xaslan Jun 18, 2026
d924413
account: drop redundant derived_id / derived_wrapper_id getters
0xaslan Jun 18, 2026
6911780
account: emit AccountCreated on derived-account creation
0xaslan Jun 18, 2026
5ade18e
account: emit app-whitelist + custody events
0xaslan Jun 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/rules/move.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ Then call as `self.id.exists_(key)`, `self.id.add(key, value)`, `self.id.borrow(
- Defaults are applied in the module that creates the config/object.
- Global template config can be snapshotted into per-object state at creation; existing objects should only change through an explicit admin path if one is intentionally added.
- Name global-template setters with `template` when the value affects future objects but not existing objects.
- `AdminCap` lives in `admin`. Public admin entrypoints live on the module that owns the mutated state: `protocol_config` for global protocol config, object modules for per-object admin state, and `registry` only for registry-owned version, pause-cap, uniqueness, and multi-object creation flows. Embedded config struct setters stay `public(package)`.
- `AdminCap` lives in `admin`. Public admin entrypoints live on the module that owns the mutated state: `protocol_config` for global protocol config (including the version watermark — `bump_version_watermark`), object modules for per-object admin state, and `registry` only for registry-owned pause-cap, lifecycle-cap, uniqueness, and multi-object creation flows. Embedded config struct setters stay `public(package)`.
- Single-value bounds live in `config_constants::assert_*`; relational checks that depend on multiple fields live in the owning config setter.
- Do not store generic `config_id` fields inside config structs or events; object identity is enough when identity matters.
- Do not add singleton creation flags for objects created during package init.
Expand All @@ -124,7 +124,7 @@ Then call as `self.id.exists_(key)`, `self.id.add(key, value)`, `self.id.borrow(
- Vocabulary: `revoke_*` removes existence-level authority from a birth allowlist; `register_*`/`unregister_*` manage per-instance membership for born-inert caps; `self_unregister_*` is the holder's possession-proved detach.
- Authorize by ID, prove by possession. Registration and seeding APIs take `ID`s because a transaction cannot reference another address's owned objects — multi-party provisioning must be by ID (e.g. `registry::revoke_lifecycle_cap`/`revoke_pause_cap` taking a cap `ID`). Object IDs are unforgeable and never reused, so seeding a wrong ID is permanently inert, never exploitable. Possession (`&Cap`) is reserved for self-actions (proof generation, self-unregister).
- `destroy` never deregisters. A stale allowlist entry left by a destroyed cap is harmless by ID-uniqueness and can be swept later via `revoke_*`/`unregister_*` by copied ID (pinned by `destroy_lifecycle_cap_does_not_revoke`). Every transferable cap gets a public `destroy`; `AdminCap` deliberately has none.
- Version-gating: mint is version-gated (granting authority under a version freeze is the risky direction); existence-level revocation never is — it is harm-reducing and must stay available even when per-object version mirrors transiently disagree (`revoke_pause_cap`, `revoke_lifecycle_cap`). Per-instance `unregister_*` may keep the guarded object's own version gate: the cap's acts and its removal read the same mirror on the same object, so they freeze atomically and no disagreement window exists. Born-inert `create` is stateless and ungated — authority is only granted through version-gated registration. Kill-switch caps (`PauseCap`) additionally bypass the mint gate so emergencies work under a version freeze.
- Version-gating: the package gate is a single monotonic watermark on `ProtocolConfig`. The invariant is **`config.assert_version()` is the first line of every `public fun` in the protocol modules (`registry`, `protocol_config`, `expiry_market`, `plp`) that takes a `&mut` protocol object, and nowhere else** — internal `*_internal`/`*_inner` helpers do not re-gate; the public caller owns it. Documented bypasses (take `&mut` but must NOT gate, so emergencies/recovery survive a freeze): the watermark setter `bump_version_watermark`, the kill switches (`mint_pause_cap`, `pause_trading_pause_cap`, `pause_expiry_market_mint_pause_cap`, `pause_mint`), and the harm-reducing revocations (`revoke_pause_cap`, `revoke_lifecycle_cap`). Mint/creation are gated (granting authority under a freeze is the risky direction). Born-inert `create` is stateless and ungated — authority arrives through version-gated registration. The per-object `allowed_versions` mirrors + `sync_*` entries are gone; do not reintroduce them.
- Cap allowlist changes emit no events today (only the manager-cap mints emit `*CapMinted`). Indexing the lifecycle/pause cap sets would require adding events first — a deliberate omission, not an oversight.

### API Shape
Expand Down
23 changes: 23 additions & 0 deletions packages/account/Move.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by move; do not edit
# This file should be checked in.

[move]
version = 4

[pinned.testnet.MoveStdlib]
source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "718ae563a42fb4ba0d055588f81c704dcef58c25" }
use_environment = "testnet"
manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363"
deps = {}

[pinned.testnet.Sui]
source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "718ae563a42fb4ba0d055588f81c704dcef58c25" }
use_environment = "testnet"
manifest_digest = "7AFB66695545775FBFBB2D3078ADFD084244D5002392E837FDE21D9EA1C6D01C"
deps = { MoveStdlib = "MoveStdlib" }

[pinned.testnet.account]
source = { root = true }
use_environment = "testnet"
manifest_digest = "5745706258F61D6CE210904B3E6AE87A73CE9D31A6F93BE4718C442529332A87"
deps = { std = "MoveStdlib", sui = "Sui" }
9 changes: 9 additions & 0 deletions packages/account/Move.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "account"
edition = "2024"
version = "0.0.1"

[dependencies]

[addresses]
account = "0x0"
255 changes: 255 additions & 0 deletions packages/account/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
# account

`account` is a reusable account package for the DeepBook ecosystem. It gives each
owner one canonical account identity for custody, app-local data, and event
attribution, while using a separate shared wrapper object to control who can borrow
the account mutably.

The package is intentionally app-agnostic. It does not know about orders, markets,
LP requests, or other protocol-specific state. Apps receive an `&mut Account` only
after the account package or registry has checked the appropriate authority.

## Modules

### `account::account`

Owns the account state and the APIs that operate on loaded accounts.

```move
public struct AccountWrapper has key {
id: UID,
account: Account,
}

public struct Account has store {
account_id: UID,
owner: address,
balances: Bag,
}
```

`AccountWrapper` is the shared object users and apps pass into entrypoints that need
to load an account. `Account` is embedded inside it and is not a standalone shared
object.

`Account.account_id()` returns the canonical account ID. This is also the
dynamic-field root used for app data. `Account.receive_address()` returns the same
identity as an address, suitable for address-balance delivery with
`balance::send_funds`.

### `account::account_registry`

Owns the derivation root and the app whitelist.

The registry creates canonical accounts:

```move
public fun new(registry: &mut AccountRegistry, ctx: &mut TxContext): AccountWrapper
public fun new_self_owned(
registry: &mut AccountRegistry,
owner_uid: &mut UID,
ctx: &mut TxContext,
): AccountWrapper
```

The returned wrapper must be shared with `account::share`.

The registry exposes both derived identities:

```move
public fun derived_address(registry: &AccountRegistry, owner: address): address
public fun derived_id(registry: &AccountRegistry, owner: address): ID
public fun derived_wrapper_address(registry: &AccountRegistry, owner: address): address
public fun derived_wrapper_id(registry: &AccountRegistry, owner: address): ID
```

`derived_address` / `derived_id` are the canonical account identity. Use these for
events, account-local storage, and address-balance delivery.
`derived_wrapper_address` / `derived_wrapper_id` identify the shared wrapper object
that must be passed to load the account.

## Identity Model

Each owner gets two derived IDs under the registry root:

- canonical account ID: app data root, address-balance receive address, and event
`account_id`
- wrapper ID: shared object handle that gates account loading

The canonical account ID is the account's public identity. The wrapper ID is an
implementation detail needed because Sui shared objects are what transactions pass
and borrow.

This split keeps address-delivered funds consistent with events: when an event
emits `account_id`, that ID's address is also where coins are delivered.

## Authority Model

Account mutation authority is represented by a mutable borrow of `Account`.

There is no `Proof`, `DataProof`, `Vault`, or `OwnerCap` in the current model. Those
were earlier designs and are not part of the source package.

Owner-controlled loading:

```move
public fun load_account(wrapper: &AccountWrapper): &Account
public fun load_account_mut(wrapper: &mut AccountWrapper, ctx: &TxContext): &mut Account
public fun load_account_mut_as_object(
wrapper: &mut AccountWrapper,
uid: &mut UID,
): &mut Account
```

`load_account_mut` checks `ctx.sender() == account.owner`. `load_account_mut_as_object`
checks that the supplied mutable `UID` belongs to the owner object address, allowing
contract-owned accounts to act through their own object.

App-controlled loading:

```move
public fun load_account_mut_as_app<App>(
registry: &AccountRegistry,
wrapper: &mut AccountWrapper,
permit: Permit<App>,
): &mut Account
```

The `Permit<App>` proves the call is executing in the module that defines `App`.
The registry whitelist decides whether that app is allowed to mutably load ecosystem
accounts.

Once a caller has `&mut Account`, coin movement and app-data mutation need no extra
account-level proof. The mutable borrow is the authority boundary.

## Coin Balances

Account balances are stored per coin type in a `Bag`.

Read path:

```move
public fun balance<T>(account: &Account): u64
```

This returns the balance already stored inside the account.

Explicit address-balance claim path:

```move
public fun claimable<T>(account: &Account, root: &AccumulatorRoot): u64
public fun settle<T>(account: &mut Account, root: &AccumulatorRoot): u64
```

`claimable` reads settled address-balance funds for the account receive address.
`settle` withdraws those funds from the address balance and deposits them into
stored account custody.

Write paths:

```move
public fun deposit<T>(account: &mut Account, coin: Coin<T>)

public fun withdraw<T>(
account: &mut Account,
amount: u64,
ctx: &mut TxContext,
): Coin<T>
```

Both write paths operate only on stored balances. Address-delivered funds sent to
`receive_address()` are intentionally not folded into `balance`, `deposit`, or
`withdraw` in the current source; callers explicitly claim them with `settle`.

## Deferred Auto Settlement

Passive accumulator settlement is intentionally removed for now. The current source
keeps settlement as an explicit direct claim (`settle<T>`) because the Sui
framework requires callers to thread `AccumulatorRoot` explicitly, which would
otherwise leak settlement plumbing through every app entrypoint and helper that
might read or mutate coin balances.

Before mainnet, after Sui exposes the accumulator root through `TxContext`, Account
will reintroduce passive settlement behind the same account APIs. At that point,
coin reads and writes can incorporate address-delivered funds without adding an
explicit `AccumulatorRoot` parameter to Predict, Account, or other ecosystem app
surfaces.

## App Data

Apps can attach one opaque data slot per app witness type:

```move
public fun attach<App, Data: store>(account: &mut Account, permit: Permit<App>, data: Data)
public fun has_data<App>(account: &Account): bool
public fun borrow_data<App, Data: store>(account: &Account): &Data
public fun borrow_data_mut<App, Data: store>(
account: &mut Account,
permit: Permit<App>,
): &mut Data
public fun detach<App, Data: store>(account: &mut Account, permit: Permit<App>): Data
```

The slot is keyed by `DataKey<App>` under the canonical account ID. Reads are open
because on-chain state is public. Writes require `Permit<App>`, so only the app's
own module can mutate its slot.

Apps that want lazy account-local state should put the ensure/create logic inside
their own account-data helper. Callers should not have to know whether the app data
has already been attached.

## App Whitelist

The registry admin controls which app witness types can mutably load accounts:

```move
public fun authorize_app<App>(registry: &mut AccountRegistry, cap: &AccountAdminCap)
public fun deauthorize_app<App>(registry: &mut AccountRegistry, cap: &AccountAdminCap)
public fun is_app_authorized<App>(registry: &AccountRegistry): bool
public fun assert_app_is_authorized<App>(registry: &AccountRegistry)
```

The whitelist is intentionally registry-scoped and ecosystem-wide. It is not a
per-account opt-in list.

## Typical Flow

EOA-owned account:

```move
let wrapper = account_registry::new(registry, ctx);
account::share(wrapper);

// Later, in a PTB or app entrypoint:
let account = account::load_account_mut(&mut wrapper, ctx);
some_app::do_something(account, ...);
```

Object-owned account:

```move
let wrapper = account_registry::new_self_owned(registry, &mut owner_uid, ctx);
account::share(wrapper);

// Later, from the owner object's module:
let account = account::load_account_mut_as_object(&mut wrapper, &mut owner_uid);
some_app::do_something(account, ...);
```

Whitelisted app:

```move
let account = account_registry::load_account_mut_as_app<MyApp>(
registry,
&mut wrapper,
permit<MyApp>(),
);
```

## Build

```bash
sui move build --path packages/account --warnings-are-errors
```

The package currently has no dedicated unit tests.
Loading
Loading