Skip to content

Commit 725ac41

Browse files
authored
docs: add consolidated trust-model page
1 parent c07ab36 commit 725ac41

2 files changed

Lines changed: 164 additions & 0 deletions

File tree

apps/docs/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export default defineConfig({
2424
{ text: "Introduction", link: "/overview/introduction" },
2525
{ text: "Why Meridian", link: "/overview/why-meridian" },
2626
{ text: "How It Works", link: "/overview/how-it-works" },
27+
{ text: "Trust Model", link: "/overview/trust-model" },
2728
],
2829
},
2930
{

apps/docs/overview/trust-model.md

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# Trust model
2+
3+
Meridian's vault holds real user USDC. This page states, in one place, what
4+
the `ADMIN` key can and cannot do, what happens if it's lost or compromised,
5+
and what that means for a depositor or an auditor evaluating custody risk —
6+
rather than reconstructing it from `docs/contracts.md`,
7+
`SECURITY.md`, and scattered issue threads (#557 in
8+
particular). Nothing here is new policy: it's a consolidation of decisions
9+
already made and already implemented, with citations back to the source.
10+
11+
**Current status, stated plainly:** Meridian is live on Stellar mainnet (see
12+
[Mainnet Deployment](../operations/mainnet-deployment.md#mainnet-deployment-record))
13+
holding real depositor funds. Per that page's own go-live checklist, as of
14+
this writing **no independent security audit has been commissioned, and
15+
`ADMIN` key custody has not yet been finalized to the hardware-backed or
16+
multisig standard the mainnet prerequisites call for.** This page describes
17+
what the contract's code enforces regardless of who holds `ADMIN` or how
18+
well; it does not represent that those two items are resolved. Check
19+
[Mainnet Deployment](../operations/mainnet-deployment.md#go-live-checklist)
20+
for their current status before relying on this page for a custody decision.
21+
22+
## What the admin key can do
23+
24+
A single `Address` stored as `ADMIN` gates every one of these. There is no
25+
per-action co-signer or committee check inside the contract itself — whatever
26+
`ADMIN` resolves to (a plain key, a hardware-backed key, a Stellar-native
27+
multisig account) _is_ the entire on-chain access-control model for all five:
28+
29+
- **`set_paused(bool)`** — an emergency switch that rejects new deposits
30+
while set. **Withdrawals are deliberately left callable regardless of
31+
pause state**, by design: see the doc comment on `set_paused` in
32+
`packages/contracts/vault/src/lib.rs`
33+
"a pause can never trap funds." An admin, malicious or not, cannot use
34+
pause to lock depositors out of their own funds.
35+
- **`set_adapter(new_adapter)`** — repoints the vault at a different yield
36+
adapter, but only when the vault holds no position at all
37+
(`TOTAL_SH == 0` and `ADPT_SH == 0`). This is a bootstrap/recovery tool
38+
for an empty vault, not a live-migration path — see `migrate_adapter`
39+
below for the mechanism that exists for a vault with real depositors.
40+
- **`begin_migration(new_adapter)` / `migrate_adapter(new_adapter, max_slippage_bps)`**
41+
the two-phase, live-migration path: `begin_migration` snapshots the
42+
target adapter's reported value, then `migrate_adapter` can only execute
43+
once `MIN_LEDGER_GAP` (17,280 ledgers, ~1 day) has elapsed since that
44+
snapshot, and only within `MAX_ADMIN_SLIPPAGE_BPS` (500 bps, 5%) of
45+
value loss versus the snapshot. Both limits are compiled-in constants in
46+
`packages/contracts/vault/src/storage.rs`,
47+
not admin-adjustable at call time — see the "Parameter selection" table
48+
in [Mainnet Deployment](../operations/mainnet-deployment.md#parameter-selection)
49+
for their exact values and #557, the incident that established both
50+
limits (previously: unbounded slippage, ~1-minute timelock).
51+
- **`transfer_admin(new_admin)` / `accept_admin()`** — a two-step handoff:
52+
the current admin nominates a successor, but authority only actually
53+
moves once the nominee calls `accept_admin` with their own signature.
54+
See "Key-loss and key-compromise consequences" below for what this
55+
design does and does not protect against.
56+
57+
That's the complete list. `deposit`, `withdraw`, `accrue` (the yield-accrual
58+
keeper call), and every read-only getter require no special authority beyond
59+
the caller's own signature where a signature is required at all.
60+
61+
## What the admin key structurally cannot do
62+
63+
- **There is no upgrade entry point, for the admin or anyone else.** See
64+
"Contract immutability" in `docs/contracts.md`:
65+
no `update_current_contract_wasm` or equivalent exists anywhere in the
66+
vault or either adapter. An admin-gated `upgrade()` was deliberately
67+
rejected specifically because it would compound the exact admin-authority
68+
risk this page documents, for a problem `migrate_adapter`/full-cutover
69+
redeployment already solves. Nothing on this page changes if the admin
70+
key is compromised, because there is no code-level lever labeled
71+
"upgrade" for a compromised key to pull.
72+
- **Admin actions cannot touch individual depositor balances directly.**
73+
`set_adapter`/`migrate_adapter` move the vault's _aggregate_ adapter
74+
position; per-depositor accounting (`Principal`, `Entry`, mUSDC balances)
75+
is denominated in vault shares and is untouched by either call — see
76+
"Vault (`meridian-vault`)" in `docs/contracts.md`.
77+
There is no admin call that debits one depositor's shares or mints
78+
uncollateralized ones.
79+
- **`migrate_adapter` cannot move funds with unlimited loss, and cannot
80+
move them instantly.** `MAX_ADMIN_SLIPPAGE_BPS` bounds the loss a single
81+
call can authorize regardless of the `max_slippage_bps` argument passed;
82+
`MIN_LEDGER_GAP` bounds how soon after `begin_migration` it can execute
83+
at all. Both are compiled into the deployed WASM, not runtime-configurable
84+
by `ADMIN` or anyone else without a full redeploy.
85+
- **Pause cannot trap funds.** Covered above, worth restating here: pause
86+
is a deposit-only brake, not a custody mechanism.
87+
88+
## Key-loss and key-compromise consequences
89+
90+
**There is no on-chain recovery from a lost or destroyed `ADMIN` key.**
91+
`transfer_admin` requires the _current_ admin's `require_auth()` (see the
92+
function's doc comment in
93+
`packages/contracts/vault/src/lib.rs`).
94+
If that key is gone before a successor is nominated and has accepted, no
95+
contract-level path replaces it. This is the same trade-off
96+
`docs/contracts.md` makes
97+
for code immutability, applied to the admin role specifically: any recovery
98+
mechanism that didn't require the current admin's own signature would itself
99+
be a takeover path for anyone else who found it. `deposit`/`withdraw` are not
100+
admin-gated at all, so depositor funds remain accessible even if `ADMIN`
101+
becomes permanently unreachable — what's lost is the ability to pause, swap
102+
adapters, or migrate going forward, not depositor access to their own funds.
103+
104+
**A compromised `ADMIN` key cannot drain the vault in a single
105+
transaction**, per the bounds in "What the admin key can do" above: a
106+
`migrate_adapter` call is capped at 5% loss versus the pre-migration
107+
snapshot and cannot execute until a day after the matching `begin_migration`
108+
call. The vault's own doc comments name this explicitly. `begin_migration`'s
109+
doc comment states plainly that the delay itself is
110+
`the only thing standing between a leaked key and the vault's entire
111+
position moving to an address the attacker controls`; `migrate_adapter`'s
112+
own doc comment makes the same point independently, in its own words, citing the
113+
same `MIN_LEDGER_GAP` mechanism as `what actually stands between a
114+
compromised key and total loss`. Both are in
115+
`packages/contracts/vault/src/lib.rs`. The timelock is a
116+
detection-and-reaction _window_, not a mechanism that prevents the outcome
117+
outright. It only has value if something is actually watching (the
118+
[alert keeper](../operations/alert-keeper.md)) and someone is positioned to
119+
react — `migrate_adapter`'s doc comment names the same two options this page
120+
does: rotating the admin key via `transfer_admin`/`accept_admin`, or pausing
121+
deposits, before the cooldown elapses — see the
122+
"Rollback plan" section of
123+
[Mainnet Deployment](../operations/mainnet-deployment.md#rollback-plan) for
124+
what reacting in time actually involves, and the incident-response runbook
125+
tracked as **#721** (not yet written as of this page) for the operational
126+
playbook once it exists. This page describes what the contract enforces;
127+
#721 is where "what do we actually do right now" belongs, and duplicating
128+
that content here would only let the two drift out of sync.
129+
130+
**`set_adapter` and one-shot `set_paused` calls are not similarly bounded.**
131+
Unlike `migrate_adapter`, neither has a slippage cap or a timelock, because
132+
neither moves funds on its own: `set_adapter` only succeeds against an
133+
empty vault (see "What the admin key can do"), and `set_paused` only blocks
134+
new deposits. A compromised key exercising either causes operational
135+
disruption (a live vault effectively frozen to new deposits, or a repointed
136+
empty vault) rather than fund loss.
137+
138+
**A compromised key that also completes `transfer_admin`/`accept_admin`
139+
before the legitimate admin reacts is a full, permanent handover** — the
140+
two-step design (see "What the admin key can do") protects against a
141+
_mistyped_ successor address, not a malicious one with its own signature
142+
ready to call `accept_admin` immediately. Detecting and reacting to a
143+
suspicious `transfer_admin` nomination before its matching `accept_admin`
144+
lands is the same race the migration timelock exists for, without a
145+
compiled-in delay to widen the window — this is the highest-severity
146+
incident category #721 will need to cover.
147+
148+
## See also
149+
150+
- `docs/contracts.md` — the contract-architecture
151+
reference this page assumes as background (adapter model, share pricing,
152+
immutability rationale).
153+
- `SECURITY.md` — vulnerability _disclosure_ policy:
154+
how to report a finding, not a description of the trust model itself.
155+
- [Mainnet Deployment](../operations/mainnet-deployment.md) — the current
156+
live deployment's actual parameter values, addresses, and go-live
157+
checklist status, including the two open items ("ADMIN key custody" and
158+
"security audit") this page's "Current status" section refers to.
159+
- **#721** (incident-response runbook, not yet written) — the operational
160+
playbook for what to actually do during a live incident (a suspected key
161+
compromise, a decision to pause, rotating a keeper secret). This page
162+
documents what the contract _enforces_; #721 will document what
163+
_people_ do in response.

0 commit comments

Comments
 (0)