|
| 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