diff --git a/docs/stablecoin/README.md b/docs/stablecoin/README.md
index 69062673..07133409 100644
--- a/docs/stablecoin/README.md
+++ b/docs/stablecoin/README.md
@@ -23,6 +23,7 @@
- [5.1 Fixed point](#51-fixed-point)
- [5.2 `compound_rate`](#52-compound_rate)
- [5.3 Current value projections](#53-current-value-projections-read-on-the-hot-path)
+ - [5.4 Wall-clock time](#54-wall-clock-time)
6. [Math](#6-math)
- [6.1 Nominal debt](#61-nominal-debt)
- [6.2 Collateralization invariant](#62-collateralization-invariant)
@@ -391,6 +392,35 @@ Where `mul_div(a, b, c) = (a * b) / c` computed via u256 to avoid intermediate o
**In plain English:** instead of writing "current_accumulator = X" to disk on every block (which would require touching every position's account on every fee tick), we store an **anchor** plus the **per-millisecond rate**, and compute the current value on the fly by rolling the anchor forward via `compound_rate`. Reads are slightly more expensive (one `compound_rate` call); writes happen only on real anchor updates (the pokes in §10.2 / §10.3).
+### 5.4 Wall-clock time
+
+Every `now` in this document is a Unix **millisecond** timestamp read from the
+system clock account, `CLOCK_01`.
+
+There is no clock on the program context. The pinned `spel-framework`
+`ProgramContext` exposes only `self_program_id` and `caller_program_id`, and
+LEE's `ProgramInput` carries no timestamp. A program that needs the time must
+therefore take the clock as an **account input**, exactly as `twap_oracle` and
+`amm` already do:
+
+1. The guest entry declares a trailing `clock: AccountWithMetadata` input.
+2. The host function asserts `clock.account_id == CLOCK_01_PROGRAM_ACCOUNT_ID`
+ and that the account is initialized.
+3. It decodes `ClockAccountData` and reads `timestamp`.
+4. It echoes the clock account **unchanged** in its post-state.
+
+Two consequences worth stating explicitly, because both are easy to get wrong:
+
+- The clock counts toward an instruction's account total. Each per-instruction
+ input list in §10 includes it, and it is always listed **last**.
+- Because the post-state echoes it, the clock also counts toward the post-state
+ total an implementation returns.
+
+Instructions that do **not** reference time omit the account entirely:
+`deposit_collateral` (§10.5), `close_position` (§10.9), `freeze` / `unfreeze`
+(§10.17–10.18), and every admin setter except
+`set_stability_fee_per_millisecond` (§10.10), which auto-accrues to `now`.
+
## 6. Math
### 6.1 Nominal debt
@@ -679,7 +709,7 @@ fn initialize_program(
);
```
-**Inputs (8 accounts):**
+**Inputs (9 accounts):**
1. `admin` — authorized, becomes `ProtocolParameters.admin_account_id`. Pre-state unchanged.
2. `protocol_parameters` — uninitialized, PDA-to-claim (`hash(program_id, "PROTOCOL_PARAMETERS")`).
@@ -689,6 +719,7 @@ fn initialize_program(
6. `stablecoin_master_holding` — uninitialized, PDA-to-claim via the same chained call (Token Program API artifact; receives `total_supply = 0`, never used again).
7. `collateral_definition` — initialized, read-only; persisted into `ProtocolParameters.collateral_definition_id`. Validated as `TokenDefinition::Fungible`.
8. `market_price_oracle` — initialized, read-only. Validated: `OraclePriceAccount`, `base_asset = stablecoin_definition.account_id` (PDA derivation predicted), `quote_asset = collateral_definition.account_id`.
+9. `clock` — the system `CLOCK_01` account; read-only. Supplies `now` (see § 5.4). Echoed unchanged in the post-state.
**Outputs:**
@@ -706,9 +737,9 @@ fn initialize_program(
```mermaid
flowchart TD
- subgraph In["Inputs (8)"]
+ subgraph In["Inputs (9)"]
a1[admin
auth] ~~~ a2[protocol_parameters
uninit] ~~~ a3[stability_fee_accumulator
uninit] ~~~ a4[redemption_price_state
uninit]
- a5[stablecoin_definition
uninit] ~~~ a6[stablecoin_master_holding
uninit] ~~~ a7[collateral_definition
init, read] ~~~ a8[market_price_oracle
init, read]
+ a5[stablecoin_definition
uninit] ~~~ a6[stablecoin_master_holding
uninit] ~~~ a7[collateral_definition
init, read] ~~~ a8[market_price_oracle
init, read] ~~~ a9[clock
CLOCK_01]
end
subgraph Post["Post-state"]
p1[protocol_parameters
CLAIMED, all fields set] ~~~ p2[stability_fee_accumulator
CLAIMED
rate = FIXED_POINT_ONE] ~~~ p3[redemption_price_state
CLAIMED
price = initial]
@@ -723,11 +754,12 @@ flowchart TD
**Signature:** `fn accrue_stability_fee();`
-**Inputs (3 accounts):**
+**Inputs (4 accounts):**
1. `caller` — authorized; satisfies runtime's ≥1-authorized requirement. Not retained.
2. `protocol_parameters` — initialized, read-only.
3. `stability_fee_accumulator` — initialized, writable.
+4. `clock` — the system `CLOCK_01` account; read-only. Supplies `now` (see § 5.4). Echoed unchanged in the post-state.
**Output state changes:**
@@ -742,8 +774,8 @@ flowchart TD
```mermaid
flowchart TD
- subgraph In["Inputs (3)"]
- a1[caller
auth] ~~~ a2[protocol_parameters
read] ~~~ a3[stability_fee_accumulator
write]
+ subgraph In["Inputs (4)"]
+ a1[caller
auth] ~~~ a2[protocol_parameters
read] ~~~ a3[stability_fee_accumulator
write] ~~~ a4[clock
CLOCK_01]
end
subgraph Post["Post-state"]
p1[stability_fee_accumulator
anchor x compound_rate
last_accrued_at = now]
@@ -756,12 +788,13 @@ flowchart TD
**Signature:** `fn update_redemption_rate();`
-**Inputs (4 accounts):**
+**Inputs (5 accounts):**
1. `caller` — authorized.
2. `protocol_parameters` — initialized, read-only.
3. `redemption_price_state` — initialized, writable.
4. `market_price_oracle` — initialized, read-only. Must equal `protocol_parameters.market_price_oracle_id`.
+5. `clock` — the system `CLOCK_01` account; read-only. Supplies `now` (see § 5.4). Echoed unchanged in the post-state.
**Output state changes (`redemption_price_state` only):** per § 6.4.
@@ -771,8 +804,8 @@ flowchart TD
```mermaid
flowchart TD
- subgraph In["Inputs (4)"]
- a1[caller
auth] ~~~ a2[protocol_parameters
read] ~~~ a3[redemption_price_state
write] ~~~ a4[market_price_oracle
read, freshness gate]
+ subgraph In["Inputs (5)"]
+ a1[caller
auth] ~~~ a2[protocol_parameters
read] ~~~ a3[redemption_price_state
write] ~~~ a4[market_price_oracle
read, freshness gate] ~~~ a5[clock
CLOCK_01]
end
subgraph Post["Post-state"]
p1[redemption_price_state
new anchor, new rate,
new integral, last_updated_at]
@@ -787,13 +820,14 @@ flowchart TD
A convenience poke that advances **both** globals in one instruction. Because a LEZ transaction carries a single instruction, this is the only way to refresh the fee accumulator and the redemption rate in one transaction. It is **best-effort**: it always performs the fee accrual, performs the redemption update only if its interval is due and the oracle is fresh, and **skips rather than panics** otherwise. The standalone `accrue_stability_fee` (§10.2) and `update_redemption_rate` (§10.3) remain for callers that want to touch just one global — and, for `update_redemption_rate`, strict loud-failing semantics on a stale oracle (see "Why keep all three" below).
-**Inputs (5 accounts)** — the union of §10.2 and §10.3:
+**Inputs (6 accounts)** — the union of §10.2 and §10.3:
1. `caller` — authorized.
2. `protocol_parameters` — initialized, read-only.
3. `stability_fee_accumulator` — initialized, writable.
4. `redemption_price_state` — initialized, writable.
5. `market_price_oracle` — initialized, read-only. Must equal `protocol_parameters.market_price_oracle_id`.
+6. `clock` — the system `CLOCK_01` account; read-only. Supplies `now` (see § 5.4). Echoed unchanged in the post-state.
**Behavior:**
@@ -813,8 +847,8 @@ A convenience poke that advances **both** globals in one instruction. Because a
```mermaid
flowchart TD
- subgraph In["Inputs (5)"]
- a1[caller
auth] ~~~ a2[protocol_parameters
read] ~~~ a3[stability_fee_accumulator
write] ~~~ a4[redemption_price_state
write] ~~~ a5[market_price_oracle
read]
+ subgraph In["Inputs (6)"]
+ a1[caller
auth] ~~~ a2[protocol_parameters
read] ~~~ a3[stability_fee_accumulator
write] ~~~ a4[redemption_price_state
write] ~~~ a5[market_price_oracle
read] ~~~ a6[clock
CLOCK_01]
end
In --> Ins((refresh_globals
best-effort))
Ins -->|fee interval due| F[accrue — as §10.2]
@@ -826,7 +860,7 @@ flowchart TD
**Signature:** `fn open_position(position_nonce: u64, initial_collateral_amount: u128);`
-**Inputs (6 accounts):**
+**Inputs (7 accounts):**
1. `owner` — authorized; becomes `position.owner_account_id`.
2. `position` — uninitialized; PDA `hash(program_id, hash(owner.account_id, position_nonce))`.
@@ -834,6 +868,7 @@ flowchart TD
4. `user_collateral_holding` — authorized, initialized; `TokenHolding::Fungible` with `definition_id = collateral_definition.account_id` and `balance ≥ initial_collateral_amount`.
5. `collateral_definition` — initialized, read-only; must equal `protocol_parameters.collateral_definition_id`. Required by the chained `Token::InitializeAccount`.
6. `protocol_parameters` — initialized, read-only. Reads `collateral_definition_id` + `is_frozen`.
+7. `clock` — the system `CLOCK_01` account; read-only. Supplies `now` (see § 5.4). Echoed unchanged in the post-state.
**Outputs:**
@@ -850,9 +885,9 @@ flowchart TD
```mermaid
flowchart TD
- subgraph In["Inputs (6)"]
+ subgraph In["Inputs (7)"]
a1[owner
auth] ~~~ a2[position
uninit] ~~~ a3[vault
uninit]
- a4[user_collateral_holding
auth + init] ~~~ a5[collateral_definition
read] ~~~ a6[protocol_parameters
read]
+ a4[user_collateral_holding
auth + init] ~~~ a5[collateral_definition
read] ~~~ a6[protocol_parameters
read] ~~~ a7[clock
CLOCK_01]
end
subgraph Post["Post-state"]
p1[position
CLAIMED PDA
collateral_amount = initial
normalized_debt = 0] ~~~ p2[vault
CLAIMED via chained
balance = initial] ~~~ p3[user_collateral_holding
balance -= initial]
@@ -903,7 +938,7 @@ flowchart TD
**Signature:** `fn withdraw_collateral(amount: u128);`
-**Inputs (7 accounts):**
+**Inputs (8 accounts):**
1. `owner` — authorized.
2. `position` — initialized, writable; PDA verified.
@@ -912,6 +947,7 @@ flowchart TD
5. `stability_fee_accumulator` — initialized, read-only; for current accumulator → nominal debt.
6. `redemption_price_state` — initialized, read-only; for current redemption price.
7. `protocol_parameters` — initialized, read-only.
+8. `clock` — the system `CLOCK_01` account; read-only. Supplies `now` (see § 5.4). Echoed unchanged in the post-state.
**Outputs:**
@@ -925,9 +961,9 @@ flowchart TD
```mermaid
flowchart TD
- subgraph In["Inputs (7)"]
+ subgraph In["Inputs (8)"]
a1[owner
auth] ~~~ a2[position
write] ~~~ a3[vault
write] ~~~ a4[user_collateral_holding
init, destination]
- a5[stability_fee_accumulator
read] ~~~ a6[redemption_price_state
read] ~~~ a7[protocol_parameters
read]
+ a5[stability_fee_accumulator
read] ~~~ a6[redemption_price_state
read] ~~~ a7[protocol_parameters
read] ~~~ a8[clock
CLOCK_01]
end
subgraph Post["Post-state"]
p1[position
collateral_amount -= amount
collateralization check] ~~~ p2[vault
balance -= amount] ~~~ p3[user_collateral_holding
balance += amount]
@@ -941,7 +977,7 @@ flowchart TD
**Signature:** `fn generate_debt(amount: u128);`
-**Inputs (8 accounts):**
+**Inputs (9 accounts):**
1. `owner` — authorized.
2. `position` — initialized, writable; PDA verified.
@@ -951,6 +987,7 @@ flowchart TD
6. `redemption_price_state` — initialized, read-only.
7. `market_price_oracle` — initialized, read-only; for staleness gate only. Must equal `protocol_parameters.market_price_oracle_id`.
8. `protocol_parameters` — initialized, read-only.
+9. `clock` — the system `CLOCK_01` account; read-only. Supplies `now` (see § 5.4). Echoed unchanged in the post-state.
**Outputs:**
@@ -964,9 +1001,9 @@ flowchart TD
```mermaid
flowchart TD
- subgraph In["Inputs (8)"]
+ subgraph In["Inputs (9)"]
a1[owner
auth] ~~~ a2[position
write] ~~~ a3[stablecoin_definition
write, chained] ~~~ a4[user_stablecoin_holding
init]
- a5[stability_fee_accumulator
read] ~~~ a6[redemption_price_state
read] ~~~ a7[market_price_oracle
read, staleness gate] ~~~ a8[protocol_parameters
read]
+ a5[stability_fee_accumulator
read] ~~~ a6[redemption_price_state
read] ~~~ a7[market_price_oracle
read, staleness gate] ~~~ a8[protocol_parameters
read] ~~~ a9[clock
CLOCK_01]
end
subgraph Post["Post-state"]
p1[position
normalized_debt += ceil amt/acc
collateralization check] ~~~ p2[stablecoin_definition
total_supply += amount] ~~~ p3[user_stablecoin_holding
balance += amount]
@@ -980,7 +1017,7 @@ flowchart TD
**Signature:** `fn repay_debt(amount: u128);`
-**Inputs (6 accounts):**
+**Inputs (7 accounts):**
1. `owner` — authorized.
2. `position` — initialized, writable; PDA verified.
@@ -988,6 +1025,7 @@ flowchart TD
4. `user_stablecoin_holding` — authorized, initialized; `definition_id = stablecoin_definition.account_id`; same Token Program.
5. `stability_fee_accumulator` — initialized, read-only.
6. `protocol_parameters` — initialized, read-only.
+7. `clock` — the system `CLOCK_01` account; read-only. Supplies `now` (see § 5.4). Echoed unchanged in the post-state.
**Outputs:**
@@ -1001,9 +1039,9 @@ flowchart TD
```mermaid
flowchart TD
- subgraph In["Inputs (6)"]
+ subgraph In["Inputs (7)"]
a1[owner
auth] ~~~ a2[position
write] ~~~ a3[stablecoin_definition
write, chained]
- a4[user_stablecoin_holding
auth + init] ~~~ a5[stability_fee_accumulator
read] ~~~ a6[protocol_parameters
read]
+ a4[user_stablecoin_holding
auth + init] ~~~ a5[stability_fee_accumulator
read] ~~~ a6[protocol_parameters
read] ~~~ a7[clock
CLOCK_01]
end
subgraph Post["Post-state"]
p1[position
normalized_debt -= floor amt/acc] ~~~ p2[stablecoin_definition
total_supply -= amount] ~~~ p3[user_stablecoin_holding
balance -= amount]
@@ -1098,18 +1136,22 @@ Every settable thing in the protocol, what it starts as, and who/how it can chan
All seven share the same skeleton:
-**Inputs (2 base + 0-1 extras):**
+**Inputs (2 base + 0-2 extras):**
- `admin` — authorized; `admin.account_id == protocol_parameters.admin_account_id`.
- `protocol_parameters` — initialized, writable.
+Extras are per-instruction, listed in the table below. Only
+`set_stability_fee_per_millisecond` references time, so it is the only setter
+that takes the `clock` account (§ 5.4); the other six omit it.
+
**Output:** exactly the field(s) listed below are overwritten on `protocol_parameters`; everything else unchanged.
**Panics if:** `admin.is_authorized = false`; admin handle mismatch; protocol_parameters uninit / wrong owner; new value outside its sane band (§ 8).
| # | Instruction | Param(s) | Fields rewritten | Extra accounts | Special note |
|---|---|---|---|---|---|
-| 10 | `set_stability_fee_per_millisecond` | `new_rate: u128` | `stability_fee_per_millisecond` | `stability_fee_accumulator` (writable) | Auto-accrues forward at the OLD rate up to `now` first. |
+| 10 | `set_stability_fee_per_millisecond` | `new_rate: u128` | `stability_fee_per_millisecond` | `stability_fee_accumulator` (writable), `clock` | Auto-accrues forward at the OLD rate up to `now` first — hence the clock. |
| 11 | `set_minimum_collateralization_ratio` | `new_ratio: u128` | `minimum_collateralization_ratio` | — | Tightening leaves existing positions retroactively under-collateralized; they cannot increase debt or withdraw collateral until back above. No mass-liquidation here (RFP-014 out of scope). |
| 12 | `set_controller_gains` | `new_proportional_gain: i128, new_integral_gain: i128` | `controller_proportional_gain`, `controller_integral_gain` | — | Does NOT reset `controller_integral_term`. |
| 13 | `set_market_price_oracle` | (no scalar) | `market_price_oracle_id` | `new_oracle` (read-only) | Validates `OraclePriceAccount` shape, base/quote ids. `program_owner` not pinned. |
@@ -1119,7 +1161,7 @@ All seven share the same skeleton:
```mermaid
flowchart TD
- subgraph In["Base inputs (2 + 0..1)"]
+ subgraph In["Base inputs (2 + 0..2)"]
a1[admin
auth, == admin_account_id] ~~~ a2[protocol_parameters
write] ~~~ ax[stability_fee_accumulator
write — only set_stability_fee] ~~~ ay[new_oracle
read — only set_market_price_oracle]
end
subgraph Post["Post-state"]