Per state-modifying external function: which dependencies it calls, and what happens when one of them is unavailable (reverts on every call).
This is the availability reference. Adversarial analysis lives in security.md; costs that exist even when every dependency behaves honestly live in risk-disclosures.md.
Key: ✅ works during outage · ❌ halted during outage.
The table describes a typical funded, levered vault. Verdicts in the two pool columns can flip to ✅ for a vault in a degenerate state — see State dependence.
| Function | Access | Morpho | Market oracle | Yield oracle | Yield/loan pool | Collateral/loan pool |
|---|---|---|---|---|---|---|
accrueFees() |
public | ❌ | ❌ | ❌ | ✅ | ✅ |
rebalance() |
public | ❌ | ❌ | ❌ | ❌ | ✅ |
harvest(uint256) |
public | ❌ | ❌ | ❌ | ❌ | ❌ |
deposit(uint256,address) |
receiver allowlisted¹ | ❌ | ❌ | ❌ | ❌ | ✅ |
redeem(uint256,address,address) |
share owner / approved | ❌ | ❌ | ❌ | ❌ | ❌ |
redeemInKind(uint256,address,address) |
share owner / approved | ❌ | ❌ | ❌ | ✅ | ✅ |
scheduleEmergencyRecovery() |
owner | ✅ | ✅ | ✅ | ✅ | ✅ |
cancelEmergencyRecovery() |
owner | ✅ | ✅ | ✅ | ✅ | ✅ |
executeEmergencyRecovery() |
owner | ❌ | ✅ | ✅ | ✅ | ✅ |
setFeeRecipient(address) |
owner | ❌ | ❌ | ❌ | ✅ | ✅ |
setManagementFeeBps(uint16) |
owner | ❌ | ❌ | ❌ | ✅ | ✅ |
setPerformanceFeeBps(uint16) |
owner | ❌ | ❌ | ❌ | ✅ | ✅ |
setMaxSlippageBps(uint16) |
owner | ✅ | ✅ | ✅ | ✅ | ✅ |
setMaxTvl(uint256) |
owner | ✅ | ✅ | ✅ | ✅ | ✅ |
grantEarlyAccess(address) |
owner | ✅ | ✅ | ✅ | ✅ | ✅ |
revokeEarlyAccess(address) |
owner | ✅ | ✅ | ✅ | ✅ | ✅ |
transfer(address,uint256)² |
holder | ✅ | ✅ | ✅ | ✅ | ✅ |
transferFrom(address,address,uint256)² |
approved spender | ✅ | ✅ | ✅ | ✅ | ✅ |
approve(address,uint256) |
public | ✅ | ✅ | ✅ | ✅ | ✅ |
transferOwnership(address) |
owner | ✅ | ✅ | ✅ | ✅ | ✅ |
acceptOwnership() |
pending owner | ✅ | ✅ | ✅ | ✅ | ✅ |
renounceOwnership() |
owner | ✅ | ✅ | ✅ | ✅ | ✅ |
onMorphoRepay(uint256,bytes)³ |
Morpho only | — | — | — | — | — |
uniswapV3SwapCallback(int256,int256,bytes)³ |
the two configured pools | — | — | — | — | — |
¹ deposit has no gate on msg.sender. The allowlist is enforced on the receiver, via _mint → _update, so anyone may fund a deposit for an allowlisted account.
² Gated by the _update allowlist hook — both parties need earlyAccess; otherwise unmodified OpenZeppelin accounting.
³ Callbacks, listed for completeness because they are externally callable and state-modifying. They have no independent availability verdict: each is authenticated to its caller (MORPHO, or one of the two configured pools) and can only run inside a parent call the vault itself initiated, so its dependencies are already accounted for in the redeem / harvest / rebalance / deposit rows. onMorphoRepay withdraws collateral and may swap on the collateral/loan pool; uniswapV3SwapCallback only pays the pool with a safeTransfer and touches nothing else.
The slippage-protected overloads deposit(uint256,address,uint256) and redeem(uint256,address,address,uint256) are thin wrappers that call the base function and then check the output, so they inherit their base row exactly.
Three separate mechanisms hit these dependencies, and between them they leave no state in which a listed ❌ becomes a ✅:
_accrueFees()runs at the top ofdeposit,redeem,redeemInKind,rebalance,harvest,accrueFees, and the three fee setters — and nowhere else. It callsMORPHO.accrueInterestand thentotalAssets(), which reads both oracleprice()functions unconditionally — the yield-oracle read is an argument to amulDiv, so Solidity evaluates it even when the yield balance is zero.- The
logsVaultStatemodifier wrapsrebalance,harvest,deposit,redeem, andredeemInKind. It runs after the body and emitsVaultState(...), reading Morpho twice and both oracles again. This is why a zero-amountdeposit, aredeem(0, …), an in-bandrebalance, and a no-opharvestall still fail under a Morpho or oracle outage despite doing no work. - Morpho's own health check.
borrowandwithdrawCollateralcall Morpho's internal_isHealthy, which reads the market oracle wheneverborrowShares != 0. So the market-oracle ❌ ondeposit,redeem, andredeemInKindhas a second, independent cause beyond_accrueFees().
executeEmergencyRecovery is the one entry point that never reads either oracle directly: it does not accrue fees, is not wrapped in logsVaultState, and its withdrawCollateral short-circuits Morpho's health check once borrow shares are zero. That is what makes the full-recovery path oracle-independent — confirmed on a Flow mainnet fork with the market oracle forced to revert throughout.
It is blocked while debt is outstanding and collateral is nonzero, because Morpho refuses to release the collateral. With zero collateral it skips withdrawCollateral entirely and succeeds regardless of debt.
Only the two pool columns are state-dependent. Each ❌ below becomes ✅ when the corresponding swap is skipped:
| Cell | ❌ requires |
|---|---|
rebalance / yield-loan |
LTV outside the band. Delever also needs a nonzero yield balance; lever-up needs no pending recovery. In-band → no pool read. |
harvest / yield-loan and collateral-loan |
A nonzero surplus (yield > debt valued in yield) and maximumYield > 0. Otherwise the function returns before either leg. |
deposit / yield-loan |
A nonzero borrow (toBorrow > 0). A position already at or above the deposit-target LTV skips the swap. |
redeem / yield-loan |
A nonzero yield slice. |
redeem / collateral-loan |
The yield sale not exactly matching the debt slice — a surplus or a shortfall. Practically always true. |
Note that harvest's collateral-loan leg is reached even when leg 1 is skipped by the price bound: loanOut == 0 still flows into the second swap's limit computation, which reads slot0.
Honest accounting of what is actually asserted rather than derived by inspection.
FCMDependencyFailures.t.solcovers every fund-path row —deposit,redeem,redeemInKind,rebalance,harvest,executeEmergencyRecovery,accrueFees— against all five dependencies. Two cases carry most of the weight: bothrebalancebranches surviving a collateral/loan-pool outage, which is the claim behind splittingharvestout ofrebalance, andaccrueFeessurviving either pool outage, since it is the only_accrueFeesconsumer withoutlogsVaultState.EmergencyRecoveryFork.t.solconfirms, against the real deployed Morpho Blue, that the recovery path stays available with the market oracle reverting throughout.
The owner-setter and ERC-20/ownership rows are derived by reading the call graph rather than asserted — they are configuration and token plumbing, not fund-path availability.
Deposits, redemptions, rebalancing, harvesting, fee accrual, the fee setters, and recovery all halt. If it stays unavailable permanently, all vault funds are permanently stuck. Share transfers and the non-fee owner setters keep working, but there is nothing behind the shares to reach.
Deposits, redemptions, rebalancing, harvesting, accrueFees(), and the three _accrueFees-calling setters (setFeeRecipient, setManagementFeeBps, setPerformanceFeeBps) all halt. executeEmergencyRecovery remains available and can still recover all funds.
Identical blast radius to the market oracle: same set halts, executeEmergencyRecovery remains available.
Deposits, redemptions, rebalancing, and harvesting halt. redeemInKind and executeEmergencyRecovery remain available as exit paths, and every owner setter plus accrueFees() keeps working.
Redemptions and harvesting halt — both route excess loan → collateral through this pool (redeem via onMorphoRepay). Deposits, rebalancing, redeemInKind, executeEmergencyRecovery, accrueFees(), and all owner setters remain available; none of them touch it.