Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
175 changes: 175 additions & 0 deletions packages/account/ACCUMULATOR_TESTING_STATUS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# Account Accumulator Testing Status

This note captures the current state of the Account package around Sui
accumulator support, the temporary dependency choice, and the flows we still
cannot prove with package-local Move unit tests.

## Current State

`account` is wired to the Sui address-balance accumulator:

- `Account.receive_address()` returns the canonical inner account address.
- `Account.balance<T>(root, clock)` returns stored balance plus unsettled funds
visible in `AccumulatorRoot`.
- `Account.deposit<T>(coin, root, clock)` and `Account.withdraw<T>(amount, root,
clock, ctx)` passively call `settle_unchecked<T>` before changing stored
balances.
- `settle_unchecked<T>` reads `balance::settled_funds_value<T>(root,
account_id.to_address())`, withdraws from the account object's address
balance, redeems the withdrawal, and deposits into the account's stored
balance.
- The last settlement timestamp is tracked per coin type with `CoinKey<T>` in a
`Bag`. If a second transaction reaches settlement for the same coin at the
same `Clock.timestamp_ms()`, Account skips the accumulator withdrawal attempt.

Predict currently relies on two address-balance paths:

- Account receives PLP / DUSDC / DEEP through `Account.receive_address()`.
- Builder-code fees are sent to the `BuilderCode` object address and claimed with
`builder_code::claim_all_builder_fees`.

## Temporary Sui Dependency

The current stable/testnet framework lock used by this repo does not expose
`sui::accumulator::create_for_testing`. That `#[test_only]` shim — a wrapper around
the private genesis constructor — is the only path that builds an `AccumulatorRoot`,
and it ships only in a newer (nightly) framework. Without it, package-local tests
cannot construct an `AccumulatorRoot` at all.

### Account (old-style manifest)

`account` has an `[addresses]` block, so it is an old-style package and can pin the
framework directly. `packages/account/Move.toml` explicitly pins Sui to a nightly
main commit:

```toml
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "2e196df64878a6ee6786cf739474e8bf4a85f726" }
```

### Predict (new-style manifest) — `implicit-dependencies = false` + `override`

`predict` is a new-style package (no `[addresses]`, forced by its transitive
new-style `pyth_lazer`/`propbook` deps), so it cannot pin a system framework in
`[dependencies]` or `[dep-replacements]` ("`Sui` is a legacy system name"). It is
also not enough for `account` to carry the nightly framework: a dependency's
test-only code (and the framework `#[test_only]`s it calls) is stripped when the
dependency is built for a dependent, so Predict tests cannot reach
`create_for_testing` through `account`. Predict's OWN canonical framework must carry
the constructor. The mechanism that works:

```toml
[package]
implicit-dependencies = false

[dependencies]
sui = { git = "...sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "2e196df6...", override = true }
std = { git = "...sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "2e196df6...", override = true }
```

This makes nightly the canonical `sui`/`std` that Predict (and `account`) bind to, so
Predict's root test code can call `accumulator::create_for_testing` directly. The
other in-repo deps (deepbook/propbook/dusdc/fixed_math/block_scholes_oracle) keep the
repo-standard testnet framework (`Sui_1` in the lock); both revisions are `0x2` with
identical manifest digests, so their types link. The split is a deliberate, isolated
dual-pin, not resolver churn.

### Reverting before mainnet

These are temporary development dependencies. Once a stable Sui release exposes an
accumulator test constructor, we should:

1. Drop Account's explicit Sui pin and Predict's `implicit-dependencies = false` +
`sui`/`std` overrides, restoring the implicit testnet framework everywhere.
2. Regenerate Account and downstream package lockfiles from that stable release.
3. Confirm a single framework revision across Predict's lockfile.
4. Re-run Account tests and the relevant Predict build/test suites.

## What Is Tested Now

`packages/account/tests/account_tests.move` currently covers:

- one canonical account per owner;
- canonical account ID, wrapper ID, owner, and receive-address derivation;
- duplicate account creation aborts;
- owner auth from `TxContext.sender()`;
- wrong-sender auth aborts;
- self-owned account auth from mutable owner `UID`;
- app whitelist authorization and deauthorization errors;
- whitelisted app auth using `Permit<App>`;
- app data attach, read, mutable read, detach, and namespace separation;
- direct Account coin deposit / withdraw with `AccumulatorRoot` and `Clock`
threaded through the public APIs;
- insufficient stored balance aborts.

The tests verify that Account's public coin APIs are shaped correctly around
`AccumulatorRoot`, but they do not verify nonzero accumulator settlement.

## What Predict Can Now Test (empty root)

With the `override` framework pin above, Predict tests construct an **empty**
`AccumulatorRoot` directly (`accumulator::create_for_testing`). Every flow whose
funds move through the account's **stored** balance — the empty-root settle is a
no-op — is therefore unit-testable end to end: account creation/deposit, mint,
live and settled redeem (paid from stored balance), liquidation cleanup, fee and
penalty accounting, and the PLP genesis lock / flush / valuation paths that read
stored balance. These are covered by the Predict flow suite.

## Flows Not Tested Yet

The remaining gap is specifically the **nonzero** address-balance (barrier) path,
which an empty root cannot exercise:

- `balance::send_funds<T>(..., account.receive_address())` becoming visible in
`balance::settled_funds_value<T>(&root, account.receive_address())`.
- `Account.balance<T>` adding a nonzero unsettled accumulator amount to stored
balance.
- `Account.deposit<T>` / `Account.withdraw<T>` settling a nonzero accumulator
amount into stored balance before applying the requested operation.
- `balance::withdraw_funds_from_object<T>(&mut account_id, amount)` succeeding
against real accumulator-delivered funds.
- The same-timestamp guard preventing duplicate settlement attempts when two
parallel transactions see the same checkpoint-settled funds.
- PLP async fills and refunds that send DUSDC / PLP to `Account.receive_address`
and depend on a later Account read/write to observe or settle those funds.
- Builder-code fee visibility and claims after Predict sends builder fees to a
builder-code object address.

The blocker is not Account source structure. We can create `AccumulatorRoot` with
the nightly framework, but external package tests cannot populate it the same way
the system settlement barrier does. The relevant settlement functions live in
`sui::accumulator_settlement`, are not public to Account, and are system-gated.
Advancing a unit-test scenario through `@0x0` after `balance::send_funds` did not
make `settled_funds_value` observe the sent funds.

## How To Close The Gap

Before mainnet, we need one of these:

- stable Sui framework support that exposes an official test path for settling
accumulator funds into `AccumulatorRoot`; or
- an integration/localnet test that exercises real checkpoint settlement rather
than Move unit-test-only state.

Once that exists, add focused tests for Account's nonzero settlement behavior
first, then cover the Predict surfaces that depend on it: PLP fill/refund delivery
to Account and builder-code fee claiming.

## Account events + indexer suite

`account::account_events` emits the account-domain events (lifecycle:
`AccountCreated`, `AppAuthorized`, `AppDeauthorized`; custody: `Deposited`,
`Withdrawn`, `FundsSettled`), indexed by the standalone `account-{schema,
indexer,server}` crates (separate tables / watermark namespace / process from
Predict). See `crates/account-server/API.md` for the served endpoints.

The Move tests in `account_tests.move` assert `AccountCreated`,
`AppAuthorized`/`AppDeauthorized`, `Deposited`, and `Withdrawn` fire with the
expected fields (all reachable against the empty test root). **Nonzero
`FundsSettled` emission stays in the deferred-coverage gap above** — it needs
barrier-delivered funds, the same system-settlement path no Move unit test can
populate. A deposit/withdraw against the empty root settles nothing, so it emits
zero `FundsSettled` (asserted). The Rust side covers `FundsSettled`'s decode →
row mapping and its fold into `account_balance` regardless
(`crates/account-indexer/tests/`), so the only untested link is the on-chain
emission of a nonzero settlement — closed by the same localnet/integration work
the gap above describes.
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" }
7 changes: 7 additions & 0 deletions packages/account/Move.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "account"
edition = "2024"
version = "0.0.1"

[addresses]
account = "0x0"
Loading
Loading