Crypto Training

Mellow Flexible Vaults: Architecture, Workflows, and Security Model

A code-level deep dive into Mellow Flexible Vaults: core modules, queue-driven fund flows, curator execution, oracle reporting, and security boundaries.

Crypto Training2026-01-059 min read

Mellow Flexible Vaults are not a single vault contract. They are a modular system where accounting, risk, queue processing, oracle reporting, and strategy execution are split across specialized contracts.

This post maps that system from code, then follows funds end-to-end across deposits, strategy execution, and redemptions.

All contract links in this article point to the public repository:

Why this architecture exists#

The design goal is to separate:

  • user onboarding/offboarding (queue contracts)
  • share accounting and fees (share manager + fee manager)
  • risk limits (risk manager)
  • strategy execution (subvault + verifier + call module)
  • report ingestion and settlement cadence (oracle)

That separation lets a single core vault support multiple assets and multiple curator strategies while keeping strict role boundaries.

flowchart TB U["User"] --> DQ["DepositQueue"] U --> RQ["RedeemQueue"] DQ --> V["Vault"] V --> SM["ShareManager"] V --> FM["FeeManager"] V --> RM["RiskManager"] O["Oracle"] --> V V --> SV1["Subvault 1"] SV1 --> V V --> SV2["Subvault 2"] SV2 --> V SV1 --> P1["External Protocol A"] SV2 --> P2["External Protocol B"] C["Curator"] --> SV1 C --> SV2 V --> H["Hooks"]

Contract map by responsibility#

ResponsibilityKey contracts
Core vault compositionsrc/vaults/Vault.sol, src/modules/VaultModule.sol, src/modules/ShareModule.sol
Strategy containersrc/vaults/Subvault.sol, src/modules/SubvaultModule.sol, src/modules/CallModule.sol, src/modules/VerifierModule.sol
Call permissionssrc/permissions/Verifier.sol, src/permissions/BitmaskVerifier.sol
Async onboarding/offboardingsrc/queues/DepositQueue.sol, src/queues/RedeemQueue.sol, src/queues/Queue.sol
Oracle/report processingsrc/oracles/Oracle.sol, src/oracles/OracleHelper.sol, src/oracles/OracleSubmitter.sol
Accounting and limitssrc/managers/ShareManager.sol, src/managers/TokenizedShareManager.sol, src/managers/FeeManager.sol, src/managers/RiskManager.sol
Liquidity hookssrc/hooks/BasicRedeemHook.sol, src/hooks/RedirectingDepositHook.sol, src/hooks/LidoDepositHook.sol
Deployment wiringsrc/vaults/VaultConfigurator.sol, src/factories/Factory.sol

Roles and authority boundaries#

The ACL root sits on the Vault (ACLModule + MellowACL), and many dependent contracts check Vault roles directly.

Example: Verifier.sol enforces call policies, but allowCalls, disallowCalls, setMerkleRoot are authorized by vault().hasRole(...).

flowchart LR A["Vault DEFAULT_ADMIN_ROLE"] --> R1["Grant oracle roles"] A --> R2["Grant queue creation/removal roles"] A --> R3["Grant verifier allowlist roles"] A --> R4["Grant liquidity push/pull roles"] R1 --> O["Oracle submit/accept/security params"] R2 --> Q["Create/pause/remove queues"] R3 --> V["Verifier policy state"] R4 --> L["Move assets between Vault and Subvaults"]

Key role families:

  • Vault lifecycle and structure: CREATE_SUBVAULT_ROLE, DISCONNECT_SUBVAULT_ROLE, RECONNECT_SUBVAULT_ROLE, PUSH_LIQUIDITY_ROLE, PULL_LIQUIDITY_ROLE in VaultModule.sol
  • Queue lifecycle: CREATE_QUEUE_ROLE, SET_QUEUE_STATUS_ROLE, REMOVE_QUEUE_ROLE in ShareModule.sol
  • Oracle operations: SUBMIT_REPORTS_ROLE, ACCEPT_REPORT_ROLE, SET_SECURITY_PARAMS_ROLE in Oracle.sol
  • Strategy call permissions: CALLER_ROLE, ALLOW_CALL_ROLE, DISALLOW_CALL_ROLE, SET_MERKLE_ROOT_ROLE in Verifier.sol

Deployment workflow (what gets wired first)#

VaultConfigurator.create(...) deploys managers and oracle via factories, then vault, then back-links manager/oracle to the vault.

sequenceDiagram autonumber participant Admin as Deployer participant VC as VaultConfigurator participant SF as ShareManagerFactory participant FF as FeeManagerFactory participant RF as RiskManagerFactory participant OF as OracleFactory participant VF as VaultFactory participant Vault as Vault Admin->>VC: create(initParams) VC->>SF: create(shareManager) VC->>FF: create(feeManager) VC->>RF: create(riskManager) VC->>OF: create(oracle) VC->>VF: create(vault with manager/oracle addresses) VC->>Vault: grant initial roles (via init roleHolders) VC->>SF: setVault(vault) VC->>RF: setVault(vault) VC->>OF: setVault(vault)

Reference: VaultConfigurator.sol.

Async deposit flow (request -> report -> claim shares)#

Step-by-step#

  1. User calls DepositQueue.deposit(...).
  2. Queue checks depositor whitelist through ShareManager (isDepositorWhitelisted).
  3. Assets are received by queue and pending balances are updated in RiskManager.
  4. Oracle report arrives, ShareModule routes it to deposit queues with depositTimestamp.
  5. Queue settles eligible requests, transfers settled assets to Vault, allocates/mints shares, emits claimability.
  6. User or any caller executes claimShares(account) on Vault/ShareModule.
sequenceDiagram autonumber participant U as User participant DQ as DepositQueue participant RM as RiskManager participant O as Oracle participant V as Vault ShareModule participant SM as ShareManager U->>DQ: deposit(assets, referral, proof) DQ->>SM: isDepositorWhitelisted(user, proof) DQ->>RM: modifyPendingAssets(asset, +assets) note over DQ: request checkpoint created O->>V: handleReport(asset, price, depositTs, redeemTs) V->>DQ: handleReport(price, depositTs) DQ->>SM: allocateShares(settledShares) DQ->>SM: mint(feeRecipient, depositFeeShares) DQ->>V: transfer settled assets to vault DQ->>RM: pending -assets, vault balance +assets U->>V: claimShares(user) V->>DQ: claim(user) DQ->>SM: mintAllocatedShares(user, shares)

References:

Deposit queue state machine#

stateDiagram-v2 [*] --> NoRequest NoRequest --> Pending: deposit() Pending --> Claimable: oracle report includes request timestamp Claimable --> NoRequest: claim() Pending --> NoRequest: cancelDepositRequest() before eligible

Async redeem flow (request -> batches -> claim assets)#

RedeemQueue is batch-oriented:

  • user locks shares
  • oracle report converts pending shares into asset demand batches
  • handleBatches(n) releases only what liquid assets can satisfy
  • users claim per timestamp once batch index is handled
sequenceDiagram autonumber participant U as User participant RQ as RedeemQueue participant SM as ShareManager participant O as Oracle participant V as Vault ShareModule participant H as Redeem Hook U->>RQ: redeem(shares) RQ->>SM: lock(user, shares) RQ->>SM: mint fee shares to feeRecipient (if needed) O->>V: handleReport(asset, price, depositTs, redeemTs) V->>RQ: handleReport(price, redeemTs) RQ->>SM: burn(locked shares in manager) note over RQ: creates batch(assets, shares) RQ->>V: getLiquidAssets() V->>H: getLiquidAssets(asset) if hook configured U->>RQ: handleBatches(n) RQ->>V: callHook(demand) note over V: hook may pull from subvault wallets U->>RQ: claim(receiver, timestamps[]) RQ-->>U: transfer assets

References:

Important nuance: what the hook can and cannot pull#

BasicRedeemHook can pull balances already sitting in subvault addresses. It does not itself unwind Aave/Morpho/other positions.

So if assets are in external protocols, curator automation must first execute protocol-specific withdrawals through Subvault call(...), then hook/vault pull can move those returned tokens back.

flowchart LR RQ["RedeemQueue demand"] --> H["BasicRedeemHook"] H -->|pull liquid token balance| SV["Subvault wallet"] SV -->|only after curator executes withdraw calls| EP["External protocol position"] note1["No direct external unwind in BasicRedeemHook"] --- H

Curator strategy execution path (how arbitrary protocols are reached)#

Subvault does not have protocol adapters hardcoded. It exposes generic call(where, value, data, payload) through CallModule.

The security boundary is Verifier:

  • caller must have CALLER_ROLE
  • call must match allowlist policy (compact/onchain, merkle compact, merkle extended, custom verifier)
sequenceDiagram autonumber participant C as Curator Executor participant SV as Subvault participant VM as Verifier participant V as Vault ACL participant P as External Protocol C->>SV: call(where, value, data, verificationPayload) SV->>VM: verifyCall(caller, where, value, data, payload) VM->>V: hasRole(CALLER_ROLE, caller)? VM->>VM: verify allowlist or merkle proof alt verification fails VM-->>SV: revert VerificationFailed else verification passes SV->>P: functionCallWithValue(where, data, value) P-->>SV: result bytes end

References:

Oracle model: push-based reports with bounded deviations#

The Oracle contract does not pull Chainlink/Aave prices by itself. It accepts submitted Report[], applies timing/deviation checks, and triggers settlement on the vault.

Suspicious reports are stored but not settled until explicit acceptance.

stateDiagram-v2 [*] --> NoPreviousReport NoPreviousReport --> Suspicious: first report (marked suspicious) Suspicious --> Active: acceptReport() Active --> Active: submit valid non-suspicious report Active --> Suspicious: submit suspicious-but-valid report Active --> Rejected: submit invalid report Suspicious --> Rejected: mismatch in accept params
sequenceDiagram autonumber participant S as Report Submitter participant O as Oracle participant V as Vault ShareModule participant Q as Queues participant A as Accepter S->>O: submitReports([asset, price]...) O->>O: validate timeout + deviation alt non-suspicious O->>V: handleReport(asset, price, depTs, redTs) V->>Q: queue.handleReport(...) else suspicious O-->>S: report stored as suspicious end A->>O: acceptReport(asset, price, timestamp) O->>V: handleReport(...) after acceptance

References:

Risk model and limit enforcement#

RiskManager tracks:

  • vault-level share-denominated exposure limit
  • per-subvault share-denominated limits
  • pending queue balances
  • allowed assets per subvault

All asset deltas are converted to shares using latest non-suspicious oracle report (convertToShares), so limits are enforced in a common unit.

flowchart TB PA["pendingAssets(asset)"] --> CS["convertToShares(report.priceD18)"] VB["vault balance changes"] --> CS SB["subvault balance changes"] --> CS CS --> VL["vaultState.limit check"] CS --> SL["subvaultState.limit check"] AL["allowedAssets(subvault)"] --> SL VL --> EVT["events + updated balances"] SL --> EVT

Reference: RiskManager.sol.

Fee model and where fees are minted#

Fees are share-based in core flows:

  • deposit fee: applied during deposit queue settlement (calculateDepositFee)
  • redeem fee: applied during redeem request (calculateRedeemFee)
  • performance/protocol fees: minted on base asset report handling (calculateFee)
sequenceDiagram autonumber participant O as Oracle participant V as ShareModule participant FM as FeeManager participant SM as ShareManager participant F as FeeRecipient O->>V: handleReport(baseAsset, price,...) V->>FM: calculateFee(vault, baseAsset, price, circulatingShares) FM-->>V: feeShares V->>SM: mint(FeeRecipient, feeShares) V->>FM: updateState(baseAsset, price) SM-->>F: increased share balance

Reference: FeeManager.sol.

Queue variants and execution patterns#

Mellow ships multiple queue modes:

  • asynchronous request/claim queues: DepositQueue, RedeemQueue
  • synchronous variants: SyncDepositQueue
  • signature-authorized order execution: SignatureDepositQueue, SignatureRedeemQueue using Consensus
flowchart LR A["Async queues"] --> A1["DepositQueue"] A --> A2["RedeemQueue"] S["Sync queues"] --> S1["SyncDepositQueue"] G["Signed order queues"] --> G1["SignatureDepositQueue"] G --> G2["SignatureRedeemQueue"] G1 --> C["Consensus threshold signatures"] G2 --> C

References:

End-to-end fund flow (macro view)#

sequenceDiagram autonumber participant User participant DQ as DepositQueue participant Vault participant SV as Subvault participant Proto as External Protocol participant RQ as RedeemQueue User->>DQ: request deposit(asset) DQ->>Vault: settle after oracle report Vault->>SV: pushAssets (optional allocation) SV->>Proto: curator-approved protocol calls Note over Proto,SV: yield accrues in position SV->>Proto: curator withdraw/unwind Proto-->>SV: assets returned SV-->>Vault: pullAssets User->>RQ: request redeem(shares) RQ->>Vault: handle batches RQ-->>User: claim redeemed assets

Security considerations and real trust assumptions#

1) Oracle trust is bounded, not eliminated#

Oracle.sol enforces role checks and deviation windows. It does not cryptographically prove that each report was sourced from a specific feed. Governance chooses submitters and acceptance operators.

2) Curator freedom is policy-constrained#

Subvault calls are generic, but verifier policies tightly gate who can call which target/selectors/calldata classes. The policy surface is critical and should be managed with timelocks, diff tooling, and monitoring.

3) Liquidity liveness depends on unwind operations#

User redemption safety has two legs:

  • on-chain queue accounting correctness
  • operational ability to unwind external protocol positions in time

If curators stop unwinding, queues can stall even when accounting is correct.

4) Role hygiene matters as much as Solidity#

Most catastrophic failures in modular vault systems come from role misconfiguration, not arithmetic bugs.

flowchart TB R["Top Risk Domains"] --> R1["ACL and role misconfig"] R --> R2["Oracle and report failures"] R --> R3["Strategy unwind failures"] R --> R4["Upgrade and deployment mistakes"] R1 --> R11["oracle submitter abuse"] R1 --> R12["verifier policy abuse"] R1 --> R13["liquidity movement abuse"] R2 --> R21["stale reports"] R2 --> R22["suspicious report handling bottleneck"] R3 --> R31["external protocol illiquidity"] R3 --> R32["curator inactivity"] R4 --> R41["wrong init params"] R4 --> R42["wrong role holders"]

Practical use cases this architecture enables#

  • institutional curator vaults where users deposit into core vault and strategy execution is delegated
  • multi-asset entry/exit with unified share accounting
  • policy-enforced execution over multiple external protocols from isolated strategy accounts
  • queue-based settlement windows suited for strategies that cannot guarantee instant withdrawals

Final mental model#

Mellow Flexible Vaults are best understood as an orchestration layer:

  • queues encode user intent over time
  • oracle reports define settlement points
  • risk manager keeps a share-denominated exposure ledger
  • subvaults execute strategy logic through guarded generic calls

If you audit or integrate this system, focus first on cross-contract invariants and role topology, then on per-contract correctness.