Docs

Architecture

This is the technical track. It assumes familiarity with ERC-4626, EVM tooling, and the basics of Superfluid. For a plain-language overview, see the user docs.

Scope: the frontend targets the Sync vault family on Base mainnet. An Async (ERC-7540) family exists in the contracts repo but is out of scope here.

The big picture

SuperVault wraps an external ERC-4626 yield source (a Morpho Vault V2) and streams a stable share of its yield to depositors using a Superfluid GDA pool (General Distribution Agreement). Three roles divide the work:

  • StableYieldSyncVault — an ERC-4626 vault. A thin share-accounting face: it mints/burns shares and forwards capital decisions to its FundManager.
  • SyncFundManager — the sole capital custodian. Holds the Morpho vault shares plus a super-token yield reserve, and drives the stream.
  • FundManagerBase — shared base holding the GDA streaming engine, used by both the Sync and Async families.

The vault deploys its FundManager in its constructor (msg.sender == vault). The pair is immutable — no factory, no proxy, no upgrade path.

Capital flow

 depositor ──USDC──▶ StableYieldSyncVault ──▶ SyncFundManager
                        │ mints shares          │
                        │                       ├──▶ Morpho Vault V2 (earns yield)
                        │                       └──▶ USDCx reserve (pre-funds streams)
                        ▼
                  GDA YIELD_POOL ──USDCx stream──▶ depositor's wallet

The vault never holds capital at rest; the FundManager is the custodian. The depositor receives ERC-4626 shares and GDA pool units.

Two things you hold

A position has two independent components:

ComponentTracksMechanism
SharesPrincipal + surplus (NAV)ERC-4626 balanceOf / previewRedeem
Pool unitsThe streamed yieldSuperfluid GDA pool membership

This separation is the core design idea: shares carry the upside, the stream carries the steady income, and the two are never double-counted.

GDA pool mechanics

The yield is distributed through a Superfluid GDA pool (YIELD_POOL). A second pool (FEE_POOL) carries the protocol fee.

  • Units are granted on nominal principal. A deposit of 100 USDC mints units = assets / RAW_PER_UNIT — independent of NAV. Units follow shares on transfer.

  • Per-unit flow rate is derived from the operator-set stable rate:

    flowRatePerUnit = SCALING_FACTOR × stableYieldRate / (YEAR × BP_DENOMINATOR)
    

    where SCALING_FACTOR = 10^(18 − assetDecimals) = 10^12 for USDC, YEAR = 31_536_000 seconds, and BP_DENOMINATOR = 10_000.

  • Total stream = flowRatePerUnit × POOL.getTotalUnits().

  • A member's accrued yield is read from getTotalAmountReceivedByMember() (monotonic, identical whether or not the member is "connected"). To make the USDCx count toward their wallet balance, a member calls connectPool() — the deposit macro does this automatically.

How yield is composed

Total yield has two non-overlapping parts:

  1. Promised stable ratestableYieldRate (basis points, set by an operator via setStableYieldRate()), streamed continuously through the GDA.
  2. External surplus — if the Morpho vault earns more than the promised rate, the excess accrues to share value (NAV), not to the stream.

NAV is computed as:

totalAssets = externalVault.previewRedeem(sharesHeldByFM)
            + scaledYieldAssetsBalance()   // the USDCx reserve
            + rawUnderlyingHeldByFM

Solvency: the reserve and guaranteed duration

Streams are pre-funded. At deposit time the FundManager upgrades part of the incoming USDC to a USDCx reserve so the GDA stream is funded forward for at least guaranteedFlowDuration (floored at MIN_GUARANTEED_FLOW_DURATION = 1 day).

Between user activity, an operator calls ensureYieldFlowDuration() to top the reserve back up and keep the stream solvent. There is no harvest() and no public fundReserve() — solvency is maintained by deposit-time pre-funding plus operator upkeep.

External pause

If the Morpho position becomes impaired or illiquid, the FundManager enters a paused state: all four max* views (maxDeposit/maxWithdraw/maxRedeem/ maxMint) read 0 so gates can't revert. The stream keeps paying from the reserve until the position is liquidated. The frontend never consults the external vault's own max* views (they're hardcoded to 0).

Next