Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
18 changes: 15 additions & 3 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,21 @@ library, which manages their TTL itself.

## Authorization

Most tests sign every call with `mock_all_auths`. A refusal test then clears
all signatures with `set_auths(&[])` and checks the call fails and changes
nothing.
Authorization is tested in two separate layers.

**Who may call (roles).** Each privileged entrypoint is called by an account
that holds the role and by accounts that do not: an unrelated address, or
another authority such as governance calling a manager-only entrypoint. Role
membership is the contract's real state, not a mock. Most of these tests run
with `mock_all_auths`, so every signature is granted and a refusal can only
come from the role check.

**Whether a signature is required.** A refusal test clears all signatures with
`set_auths(&[])` and checks the call fails and changes nothing, which proves the
entrypoint requires the right address to sign.

The per-entrypoint role matrix, with the refusal test for each, is in
[AUTHORIZATION_MATRIX.md](./AUTHORIZATION_MATRIX.md).

| Behaviour | Tests | Key test names |
| --- | --- | --- |
Expand Down
49 changes: 27 additions & 22 deletions docs/kit-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,28 @@ It covers:
The contracts enforce a set of mathematical and state-machine invariants across all operations. Each invariant is stated below as a formal, testable property matching the on-chain test suite.

### Invariant 1: Escrow Stays, Only Free Reserve Moves
- **Definition**: Cancellable subscription escrow never leaves the vault, and only the free reserve can move to the custodian. Priced redemption liabilities (`committed`) may exceed the liquid reserve; the shortfall is reported as `uncovered`, not refused. Here $\text{held}$ is the vault's balance of the settlement asset.
- **Definition**: Cancellable subscription escrow never leaves the vault, and only the free reserve can move to the custodian. Priced redemption liabilities (`committed`) may exceed the liquid reserve; the shortfall is reported as `uncovered`, not refused. Here $`\text{held}`$ is the vault's balance of the settlement asset.
- **Mathematical Statement**:
$$\text{liquid\_reserve} = \text{held} - \text{cancellable\_escrow}$$
$$\text{free\_reserve} = \max(\text{liquid\_reserve} - \text{committed} - \text{wind\_down\_owed}, 0)$$
$$\text{uncovered} = \max(\text{committed} - \text{liquid\_reserve}, 0)$$
- **Testable Property**: `deploy_to_custodian(assets)` is refused with `ReserveCommittedToExits` when $\text{assets} > \text{free\_reserve}$, so after every operation $\text{held} \ge \text{cancellable\_escrow}$. While $\text{uncovered} > 0$, the free reserve is zero and nothing leaves for the custodian.
$`\text{liquid\_reserve} = \text{held} - \text{cancellable\_escrow}`$

$`\text{free\_reserve} = \max(\text{liquid\_reserve} - \text{committed} - \text{wind\_down\_owed}, 0)`$

$`\text{uncovered} = \max(\text{committed} - \text{liquid\_reserve}, 0)`$
- **Testable Property**: `deploy_to_custodian(assets)` is refused with `ReserveCommittedToExits` when $`\text{assets} > \text{free\_reserve}`$, so after every operation $`\text{held} \ge \text{cancellable\_escrow}`$. While $`\text{uncovered} > 0`$, the free reserve is zero and nothing leaves for the custodian.
- **Enforcement**: `deploy_to_custodian` is the only outbound path to the custodian, and it moves at most the free reserve.

### Invariant 2: Notice Period Respected
- **Definition**: An epoch cannot be fulfilled or priced until its standing notice period has fully elapsed.
- **Mathematical Statement**:
$$\text{priceable\_at} = \text{closed\_at} + \text{notice}$$
$$\text{fulfill\_epoch}(\text{epoch}) \implies \text{ledger.timestamp} \ge \text{priceable\_at}$$
$`\text{priceable\_at} = \text{closed\_at} + \text{notice}`$

$`\text{fulfill\_epoch}(\text{epoch}) \implies \text{ledger.timestamp} \ge \text{priceable\_at}`$
- **Testable Property**: Invocations of `fulfill_epoch` before `priceable_at` are rejected with `NoticeNotElapsed`. Changing the notice period via `set_notice` applies only to future epochs and never shortens the notice of an already closed epoch.

### Invariant 3: Post-Close Valuation Timing
- **Definition**: An epoch is never priced against a valuation accepted before that epoch closed.
- **Mathematical Statement**:
$$\text{attested\_at} \ge \text{closed\_at}$$
$`\text{attested\_at} \ge \text{closed\_at}`$
- **Testable Property**: If the latest oracle attestation timestamp precedes `closed_at`, `fulfill_epoch` is rejected with `AttestationBeforeClose`. A valuation accepted in the exact closing ledger timestamp or later is accepted.

### Invariant 4: Covered Claims Always Pay (Exit Guarantee)
Expand Down Expand Up @@ -62,25 +65,27 @@ The contracts enforce a set of mathematical and state-machine invariants across
### Invariant 7: Directional Rounding & Non-Creation of Value
- **Definition**: Arithmetic conversions round in the vault's favor, preventing arbitrage and value creation from rounding residue.
- **Mathematical Statement**:
$$\text{assets\_to\_shares}(a) = \lfloor (a \cdot \text{SCALE}) / \text{NAV} \rfloor$$
$$\text{shares\_to\_assets}(s) = \lfloor (s \cdot \text{NAV}) / \text{SCALE} \rfloor$$
$$\text{assets\_to\_shares}(\text{shares\_to\_assets}(s)) \le s$$
- **Testable Property**: For all values of $a, s \in [1, 10^{30}]$ and $\text{NAV} \in [\text{min\_answer}, \text{max\_answer}]$, a round-trip conversion never creates value. Remainder satisfies:
$$(a \cdot \text{SCALE}) - (s \cdot \text{NAV}) < \text{NAV}$$
$`\text{assets\_to\_shares}(a) = \lfloor (a \cdot \text{SCALE}) / \text{NAV} \rfloor`$

$`\text{shares\_to\_assets}(s) = \lfloor (s \cdot \text{NAV}) / \text{SCALE} \rfloor`$

$`\text{assets\_to\_shares}(\text{shares\_to\_assets}(s)) \le s`$
- **Testable Property**: For all values of $`a, s \in [1, 10^{30}]`$ and $`\text{NAV} \in [\text{min\_answer}, \text{max\_answer}]`$, a round-trip conversion never creates value. Remainder satisfies:
$`(a \cdot \text{SCALE}) - (s \cdot \text{NAV}) < \text{NAV}`$

### Invariant 8: Bounded Governance Timelocks
- **Definition**: Upgrades and wind-downs cannot execute without advance notice, and delays cannot be set to invalid boundaries.
- **Testable Property**:
- Upgrade delay $\in [7 \text{ days}, 90 \text{ days}]$ and $\text{delay} \ge \text{notice}$.
- Wind-down delay $\le 90 \text{ days}$.
- Applying an upgrade before $\text{proposed\_time} + \text{delay}$ reverts with `UpgradeDelayNotElapsed`.
- Upgrade delay $`\in [7 \text{ days}, 90 \text{ days}]`$ and $`\text{delay} \ge \text{notice}`$.
- Wind-down delay $`\le 90 \text{ days}`$.
- Applying an upgrade before $`\text{proposed\_time} + \text{delay}`$ reverts with `UpgradeDelayNotElapsed`.
- Pausing freezes the upgrade timelock clock and pushes the ETA by the duration paused.
- Renouncing admin is permanently refused (`AdminRequired`).

### Invariant 9: Wind-Down Pro-Rata Solvency
- **Definition**: In an active wind-down, total round payouts across all share claims can never exceed the capital credited to that round.
- **Mathematical Statement**:
$$\sum_{i} \text{payout}_i \le \text{round\_assets}$$
$`\sum_{i} \text{payout}_i \le \text{round\_assets}`$
- **Testable Property**: In any distribution round with arbitrary share distributions and rounding remainders, the vault remains solvent for the last claimant. Active wind-down is strictly irreversible.

---
Expand All @@ -104,12 +109,12 @@ Deploying the Strata Vault Kit involves five contracts configured via `environme
| **`nav_oracle`** | `admin` | `Address` | Oracle admin multisig (Governance) | `nav_oracle.constructor_args` |
| **`nav_oracle`** | `attester` | `Address` | Authorized reporter multisig for NAV attestations | `nav_oracle.constructor_args` |
| **`nav_oracle`** | `guardian` | `Address` | Guardian multisig authorized to raise ripcord | `nav_oracle.constructor_args` |
| **`nav_oracle`** | `config.freshness_duration` | `u64` (seconds) | Validity window of an attestation before it is stale ($> 0$) | `nav_oracle.constructor_args` |
| **`nav_oracle`** | `config.freshness_duration` | `u64` (seconds) | Validity window of an attestation before it is stale ($`> 0`$) | `nav_oracle.constructor_args` |
| **`nav_oracle`** | `config.cooldown_secs` | `u64` (seconds) | Minimum time between consecutive price attestations | `nav_oracle.constructor_args` |
| **`nav_oracle`** | `config.max_up_bps` | `u32` (basis points) | Maximum allowable upward price jump (1 bps = 0.01%, $> 0$) | `nav_oracle.constructor_args` |
| **`nav_oracle`** | `config.max_up_bps` | `u32` (basis points) | Maximum allowable upward price jump (1 bps = 0.01%, $`> 0`$) | `nav_oracle.constructor_args` |
| **`nav_oracle`** | `config.max_down_bps` | `Option<u32>` (bps) | Maximum allowable downward jump (`null` = loss of any size allowed) | `nav_oracle.constructor_args` |
| **`nav_oracle`** | `config.min_answer` | `i128` (18 decimals) | Absolute floor price per share (cannot be zero or negative) | `nav_oracle.constructor_args` |
| **`nav_oracle`** | `config.max_answer` | `i128` (18 decimals) | Absolute ceiling price per share ($\ge \text{min\_answer}$) | `nav_oracle.constructor_args` |
| **`nav_oracle`** | `config.max_answer` | `i128` (18 decimals) | Absolute ceiling price per share ($`\ge \text{min\_answer}`$) | `nav_oracle.constructor_args` |
| **`async_vault`** | `asset` | `Address` | Settlement asset SAC contract address (e.g. USDC) | `async_vault.constructor_args` |
| **`async_vault`** | `share_token` | `Address` | Deployed share token contract address | `async_vault.constructor_args` |
| **`async_vault`** | `oracle` | `Address` | Deployed NAV oracle contract address | `async_vault.constructor_args` |
Expand All @@ -133,11 +138,11 @@ The `async_vault` constructor strictly verifies pairwise separation across autho

| Parameter | Method | Authority | Constraints & Defaults |
|---|---|---|---|
| **Standing Notice** | `set_notice(secs: u64)` | Governance | Notice wait before pricing. Default: `0`. Must satisfy $\text{secs} \le \text{upgrade\_delay}$ and $\text{secs} \le 30 \text{ days}$ (`MAX_NOTICE_SECS`). |
| **Standing Notice** | `set_notice(secs: u64)` | Governance | Notice wait before pricing. Default: `0`. Must satisfy $`\text{secs} \le \text{upgrade\_delay}`$ and $`\text{secs} \le 30 \text{ days}`$ (`MAX_NOTICE_SECS`). |
| **Custodian Address** | `set_custodian(custodian: Address)` | Governance | Recipient address for off-chain capital deployment. Required before `deploy_to_custodian`. |
| **Deposit Cap** | `set_deposit_cap(cap: Option<i128>)` | Governance | Maximum capacity for deposited capital. `None` = unbounded. Refuses negative cap. |
| **Wind-Down Delay** | `set_wind_down_delay(secs: u64)` | Governance | Minimum notice before wind-down activation. Default: `0`. Capped at 90 days (`MAX_WIND_DOWN_DELAY`). |
| **Upgrade Delay** | `propose_upgrade_delay(secs: u64)` | Governance | Timelock delay for wasm upgrades. Must be $\ge 7 \text{ days}$, $\le 90 \text{ days}$, and $\ge \text{notice}$. |
| **Upgrade Delay** | `propose_upgrade_delay(secs: u64)` | Governance | Timelock delay for wasm upgrades. Must be $`\ge 7 \text{ days}`$, $`\le 90 \text{ days}`$, and $`\ge \text{notice}`$. |
| **Oracle Configuration** | `set_config(config: OracleConfig)` | Governance | Updates oracle parameters. Follows same validation rules as constructor. |

---
Expand Down
Loading