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
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ Meridian is a **testnet technical preview**, not a finished product. Be clear-ey
- **Deposit / withdraw through the live `MeridianVault` coordinator contract**: the vault forwards your USDC to its active adapter contract (`BlendAdapter` today), which supplies it straight into a real Blend pool — you receive mUSDC shares representing the position, no Meridian-controlled custody of the underlying funds
- Live TVL and per-address position reads directly from the vault (`get_total_assets`, `get_position`)
- Best-rate routing: the API recommends the highest-APY vault it can actually deposit into, skipping display-only protocols and pools flagged risky
- Protocol-agnostic adapter architecture: `MeridianVault` (ERC-4626-style share accounting hardened against the first-depositor inflation attack, pause + admin-rotation rails), `BlendAdapter` (live), and a `DefindexAdapter` contract (built, not yet wired to a live vault) — swapping which protocol a vault routes to is an admin-only `set_adapter` call, no vault redeploy required. A `router` contract exists reserved for a future v2 single-transaction rebalancing feature. All four have unit test coverage.
- Protocol-agnostic adapter architecture: `MeridianVault` (ERC-4626-style share accounting hardened against the first-depositor inflation attack, pause + admin-rotation rails), `BlendAdapter` (live), and a `DefindexAdapter` contract (built, not yet wired to a live vault) — swapping which protocol a vault routes to is an admin-only `set_adapter` call, no vault redeploy required. A `router` contract implements a working, tested `rebalance` entry point that atomically moves a user's position between two vault instances in one transaction — it's not yet exposed through the API or UI. All four contracts have unit test coverage.
- Per-position yield earned: cost-basis tracking via `get_principal`, surfaced in the dashboard alongside the current position value

**In progress — the core promise is not finished**

- Deposit/withdraw against a real DeFindex vault through `DefindexAdapter` — the adapter contract and transaction builders are implemented; gated behind `DEFINDEX_VAULT_ID` until a real testnet vault is wired
- Per-position yield earned (cost-basis tracking is implemented on-chain via `get_principal`; UI display is still in progress)
- Mainnet configuration and a security audit before any real-funds use

Until a DeFindex vault is configured, the DeFindex deposit path throws a configuration error rather than silently routing elsewhere. Track progress in the [Roadmap](#roadmap) and [open issues](../../issues).
Expand Down Expand Up @@ -80,7 +80,7 @@ The API never holds private keys. It builds an unsigned Soroban transaction, ret
| Frontend | Vite 8, React 19, Tailwind CSS, Zustand, TanStack Query |
| Backend (prod) | Vercel Serverless Functions, Zod validation |
| Backend (local) | Fastify |
| Blockchain | Stellar Soroban, `@stellar/stellar-sdk` v12 |
| Blockchain | Stellar Soroban, `@stellar/stellar-sdk` v14 |
| Protocols | Blend Capital, DeFindex |
| Contracts | Rust / Soroban SDK |
| Monorepo | pnpm workspaces, Turborepo |
Expand Down Expand Up @@ -168,13 +168,13 @@ Issues are tagged `good first issue`, `medium`, and `hard`. Pick your level.

Non-custodial USDC deposits into the `MeridianVault` coordinator contract on Stellar testnet, live and working end-to-end for Blend via `BlendAdapter`. Freighter wallet connects in one click, the best-rate vault is selected automatically, and the signed transaction never leaves the browser. Live APY and TVL across protocols with risk-tier labelling. Withdraw at any time, no lock-up. DeFindex support is built (`DefindexAdapter`) but not yet wired to a live testnet vault.

### Q3 2026: Yield tracking and position history
### Q3 2026: Yield history and position analytics

Per-position yield tracking with a cost-basis model so users see their actual earnings, not just current balance. A yield history chart broken down by protocol, entry time, and cumulative earned. Position-level analytics that work whether funds are in Blend, DeFindex, or split across both.
Per-position yield tracking with a cost-basis model is shipped: users already see cumulative earned alongside their current balance. Remaining: a yield history chart broken down by protocol, entry time, and cumulative earned over time. Position-level analytics that work whether funds are in Blend, DeFindex, or split across both.

### Q4 2026: Atomic rebalancing
### Q4 2026: Atomic rebalancing, exposed

A Soroban router contract that rebalances between vaults in one atomic transaction. No manual withdraw-then-deposit cycle: when a better rate appears, funds move in a single ledger close. Auto-rebalancing triggers with user-defined APY thresholds. The groundwork for supporting new protocols without UI changes.
The `router` contract's `rebalance` entry point is built and tested: it already moves a user's position between two vaults in one atomic transaction, no manual withdraw-then-deposit cycle. What's missing is exposing it: an API endpoint and UI trigger so a user can actually invoke it, plus auto-rebalancing triggers with user-defined APY thresholds. See [#469](../../issues/469) for the fully-automated (delegated) version of this.

### Q1 2027: Mainnet and scale

Expand Down
14 changes: 7 additions & 7 deletions apps/docs/architecture/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,13 @@ The Vercel functions (`api/v1/`) import workspace packages that are pre-built in

The Fastify server (`apps/api-local/`) runs the same packages directly via `tsx`, which handles TypeScript natively in the development process.

## Vault ID to protocol mapping
## Vault ID to contract address mapping

When building a deposit transaction, the `vaultId` is mapped to the `Protocol` enum expected by the vault contract:
The vault contract's `deposit`/`withdraw` take no protocol-selection parameter — which protocol a deposit reaches is fixed by whichever adapter the target vault instance has set, not by anything passed in the call. Building a deposit transaction therefore resolves `vaultId` directly to the specific deployed vault contract address to call, via the mapping in `packages/stellar-sdk-helpers/src/known-pools.ts`:

| Vault ID prefix | Protocol |
| --------------- | ---------- |
| `blend-` | `Blend` |
| `defindex-` | `DeFindex` |
| Vault ID prefix | Resolves to |
| --------------- | ---------------------------------------------------------- |
| `blend-` | A vault instance with `BlendAdapter` set as its adapter |
| `defindex-` | A vault instance with `DefindexAdapter` set as its adapter |

Any other prefix returns a 500 with a clear mapping error.
Any unrecognized `vaultId` returns a 500 with a clear mapping error.
2 changes: 1 addition & 1 deletion apps/docs/architecture/monorepo.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ meridian/
│ │
│ └── contracts/ # Rust/Soroban smart contracts
│ ├── vault/src/lib.rs # MeridianVault: protocol-agnostic coordinator
│ ├── router/src/lib.rs # Reserved for v2 atomic rebalancing
│ ├── router/src/lib.rs # MeridianRouter: atomic rebalance() between vaults, not yet exposed via API/UI
│ ├── blend-adapter/src/lib.rs # Supplies USDC into a Blend lending pool
│ └── defindex-adapter/src/lib.rs # Deposits USDC into a DeFindex vault
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/architecture/signing-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Browser API (Vercel) Stellar RPC

**The API receives only a public key, never a private key.** It uses the public key to fetch the account's sequence number from the Stellar RPC, which is needed to construct a valid transaction. The private key never leaves the user's device.

**The user sees the transaction before signing.** Freighter displays the contract address, function name, and all arguments including the routing protocol. The user can reject if anything looks wrong.
**The user sees the transaction before signing.** Freighter displays the contract address, function name, and all arguments. The user can reject if anything looks wrong.

**The API cannot forge a transaction on the user's behalf.** `caller.require_auth()` in the Soroban contract ensures the transaction is only valid if it carries a valid signature from `caller`. The API cannot produce that signature.

Expand Down
68 changes: 22 additions & 46 deletions docs/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,44 +12,28 @@ Stellar CLI (`stellar contract build`). The deploy script at

Source: [`packages/contracts/vault/src/lib.rs`](../packages/contracts/vault/src/lib.rs)

The vault is a share-based USDC custodian modelled on ERC-4626. Users deposit
USDC and receive mUSDC share tokens whose redemption value grows with yield
accrued in the underlying protocol (Blend or DeFindex).
The vault is a protocol-agnostic coordinator, not a direct protocol integration:
it holds no opinion about where funds actually earn yield and delegates all
protocol-specific work to a swappable **adapter** contract (`BlendAdapter` or
`DefindexAdapter`), selected via `set_adapter`. Users deposit USDC and receive
mUSDC share tokens whose redemption value grows with yield accrued in the
active adapter's underlying protocol.

A virtual share/asset offset of 1 000 stroops is applied to all price
calculations to neutralise the first-depositor inflation attack: an attacker
who donates USDC directly to the vault recovers only a negligible fraction of
who donates USDC directly to the adapter recovers only a negligible fraction of
the donation, making the skim unprofitable.

### Entry points
There is no `route_to` or protocol-selection parameter on `deposit` — which
protocol funds reach is determined entirely by whichever adapter the vault
currently has set, not by anything the caller passes in. Which vault _instance_
(and therefore which adapter) a deposit targets is chosen off-chain, by
resolving the `vaultId` to a specific deployed vault contract address.

| Function | Description |
| ----------------------------------- | ---------------------------------------------------------------------------------------- |
| `initialize(admin, usdc, musdc)` | One-time setup. Sets admin, USDC token, and mUSDC share token addresses. |
| `deposit(caller, amount, route_to)` | Pull `amount` USDC from `caller`, mint proportional mUSDC shares. Returns shares minted. |
| `withdraw(caller, shares)` | Burn `shares` mUSDC, send proportional USDC back to `caller`. Returns USDC sent. |
| `get_position(address)` | Current mUSDC share balance for `address`. |
| `get_principal(address)` | Net USDC deposited and not yet withdrawn (cost basis). |
| `get_entry_time(address)` | Ledger timestamp of the address's first deposit in the current position. |
| `get_active_protocol()` | The protocol hint set on the last deposit. |
| `get_total_assets()` | Total USDC held by the vault. |
| `get_total_shares()` | Total mUSDC shares outstanding. |
| `set_paused(paused)` | Admin-only. Block new deposits while leaving withdrawals open. |
| `set_admin(new_admin)` | Admin-only. Rotate the admin key without redeploying. |

### `Protocol` type

```rust
pub enum Protocol {
Blend,
DeFindex,
}
```

Passed to `deposit` to record which underlying protocol the funds should go to.
The vault stores the value but does not route funds itself - routing is the
responsibility of the off-chain API, which selects the best protocol and
includes the choice in the unsigned transaction it builds for the user to sign.
For the full entry-point reference, error codes, and storage layout, see
[`apps/docs/architecture/vault-contract.md`](../apps/docs/architecture/vault-contract.md) —
that page is the canonical, detailed reference; this page covers the router
below.

## Router (`meridian-router`)

Expand All @@ -69,11 +53,11 @@ sequenceDiagram
participant VaultA as from_vault
participant VaultB as to_vault

User->>Router: rebalance(depositor, from_vault, to_vault, shares, min_out, route_to)
User->>Router: rebalance(depositor, from_vault, to_vault, shares, min_out)
Router->>VaultA: withdraw(depositor, shares)
VaultA-->>Router: usdc_received
Note over Router: panic if usdc_received < min_out (slippage guard)
Router->>VaultB: deposit(depositor, usdc_received, route_to)
Note over Router: returns Err(RouterError::SlippageExceeded) if usdc_received < min_out
Router->>VaultB: deposit(depositor, usdc_received)
VaultB-->>Router: new_shares
Router-->>User: new_shares
```
Expand All @@ -92,8 +76,7 @@ pub fn rebalance(
to_vault: Address,
shares: i128,
min_out: i128,
route_to: Protocol,
) -> i128
) -> Result<i128, RouterError>
```

| Parameter | Description |
Expand All @@ -103,9 +86,9 @@ pub fn rebalance(
| `to_vault` | Vault contract to deposit into. |
| `shares` | mUSDC share count to burn on `from_vault`. |
| `min_out` | Minimum USDC stroops the withdrawal must return. Reverts on slippage. |
| `route_to` | Protocol hint forwarded to `to_vault.deposit`. |

Returns the number of shares minted by `to_vault`.
Returns the number of shares minted by `to_vault`, or `RouterError::SlippageExceeded`
if the withdrawal from `from_vault` returned fewer stroops than `min_out`.

### Auth model

Expand All @@ -124,13 +107,6 @@ signing, so no special handling is needed in the frontend beyond the standard
a sensible value by multiplying the current share redemption rate by
`(1 - slippage_tolerance)` and converting to stroops.

## Shared `Protocol` type

The router re-declares `Protocol` with the same `#[contracttype]` annotation and
variant names as the vault. Soroban serialises contracttype enums to XDR using
the variant name, so identical declarations produce identical wire bytes and
cross-contract calls work without sharing a crate.

## Building and deploying

```bash
Expand Down
6 changes: 3 additions & 3 deletions docs/signing-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ browser hands that XDR to the user's wallet (Freighter) for signing. The signed
relayed back through the API only to be forwarded to the Stellar network — the API still
never signs anything itself.

This document is the reference for anyone implementing the XDR builder (#14) or the Freighter
adapter (#8). Implement against this doc; you should not need to read the Soroban auth docs to
get the flow right.
This document is the reference for how the XDR builder and the Freighter adapter fit together.
Implement against this doc; you should not need to read the Soroban auth docs to get the flow
right.

## Sequence

Expand Down
Loading