Skip to main content

YDS: Mental Model & Lifecycle

Purpose: Walk through the complete lifecycle of standalone YDS strategies and MultistrategyVaults, explain required overrides, and distinguish user flows from operator flows.

Audience: Solidity developers implementing YDS strategies or operating MultistrategyVaults, who understand YDS concepts and architecture.

Level: Intermediate

Source of truth: Octant v2 BaseStrategy and MultistrategyVault implementations; Yearn V3 vault framework.

Use this page when: You're implementing the three core hooks; you need to understand report() flow; you're building a vault operator; you want to understand donation-share burning logic; you're debugging a lifecycle issue.

Do not use this page for: High-level YDS concepts (see Introduction); system architecture (see Architecture); writing production code with real external protocols (see Writing a YDS Strategy); general solidity patterns.

Before you read this page
TL;DR

Strategy authors: implement _deployFunds, _freeFunds, and _harvestAndReport. The shared YDS implementation handles profit/loss settlement at report() time, including donation-share minting and, when enableBurning is enabled, donation-share burning before residual loss reaches PPS. Vault operators: a MultistrategyVault can aggregate multiple strategies; update_debt is for allocation and rebalancing, while process_report separately books already-reported strategy P&L into vault accounting.

What this page covers

This page explains two related but distinct layers:

  1. the lifecycle of a standalone Yield Donating Strategy that users can deposit into directly,
  2. the lifecycle of a MultistrategyVault that may hold one or more YDS strategies alongside other strategy types.

Those layers interact, but they are not the same thing. In particular:

  • strategy report() settles strategy-level profit or loss,
  • vault process_report(strategy) records the result of that strategy report at the vault layer,
  • vault withdrawals use idle liquidity and withdrawal queues, not a user-facing update_debt step.

Strategy Lifecycle (Direct Deposit)

A standalone YDS strategy is an ERC-4626 vault for a single ERC-20 asset. Users interact through the normal ERC-4626 surface:

  • deposit
  • mint
  • withdraw
  • redeem
  • conversion and limit views

At the strategy-author level, the core model is intentionally small: you implement the integration-specific hooks, and the shared implementation handles ERC-4626 accounting, role-gated reporting, and donation accounting.

Strategy Lifecycle Summary

A YDS moves through several key states from initial deposit to profit or loss settlement. The following diagram shows the complete lifecycle:

Two perspectives on the lifecycle

StageUser sees...Operator does...
Deposit"I deposited assets and got strategy shares, minus the one-time 1,000-share minimum-liquidity lock if this was the first deposit"Nothing — deposits are permissionless
DeployNothing visibleStrategy auto-deploys idle funds to yield source
AccrueNothing visible (PPS stays flat)Nothing — yield accumulates in external position
Report"Donation address got new shares"Keeper calls report()
Loss"My shares are worth less DAI" (if buffer exhausted)Reviews health-check settings, investigates cause
Withdraw"I redeemed shares and received assets at the current PPS"Nothing — withdrawals are permissionless

A YDS strategy's core lifecycle comes down to three hooks you implement:

  • _deployFunds
  • _freeFunds
  • _harvestAndReport

The shared YDS implementation handles the rest:

  • ERC-4626 share accounting,
  • report-time profit/loss settlement,
  • donation-share minting on profit,
  • optional donation-share burning on loss when enableBurning is active,
  • standard role-gated operations and shutdown behavior.

Your job as a strategy author:

  1. implement the three required overrides,
  2. keep deposit and withdrawal paths simple and deterministic,
  3. make _harvestAndReport return an honest totalAssets number,
  4. optionally add deposit limits, withdrawal limits, maintenance hooks, and reporting safety checks when your integration needs them.

Standalone Strategy Lifecycle Step by Step

1. Deposit -> shares

A user calls deposit() or mint() on the strategy.

  • The shared ERC-4626 logic checks limits such as availableDepositLimit(owner).
  • User shares are minted.
  • The strategy's _deployFunds(amount) hook is called to deploy the newly available underlying into the external yield source.

On an empty YDS in 1.2.0-develop.15, the first successful deposit or mint permanently locks 1_000 shares at address(0xdead). A first deposit of assets mints assets - 1000 shares to the depositor, and a first mint(shares) requires shares + 1000 assets. Do not write tests that assume the first YDS deposit is exactly 1:1.

This path should stay boring: no swaps, no price-sensitive logic, no oracle-dependent branching.

2. Withdraw -> assets

A user calls withdraw() or redeem() on the strategy.

  • The shared ERC-4626 logic checks availableWithdrawLimit(owner).
  • If idle assets are insufficient, the strategy's _freeFunds(amount) hook is called.
  • User shares are burned and underlying is returned.

If _freeFunds cannot return the full requested amount, how the shortfall is handled depends on the entry point. The default withdraw() wraps maxLoss = 0 and reverts with "too much loss" on any shortfall; only redeem() (default maxLoss = MAX_BPS) or the explicit-maxLoss overloads will realize the loss and complete the withdrawal. Strategies with liquidity constraints should use withdrawal limits or explicit reverts instead of silently relying on incorrect assumptions.

3. Tend (optional)

If the strategy implements _tendTrigger() and it returns true, an authorized operator may call tend().

This is an optional mid-cycle maintenance path. Typical uses include:

  • deploying idle capital once a threshold is reached,
  • refreshing a position,
  • protocol-specific maintenance that should not wait for the next report.

tend() does not itself settle strategy profit/loss into donation accounting.

4. Report -> profit/loss settlement

A keeper or management calls report() on the strategy.

That call:

  1. runs the strategy's _harvestAndReport() hook,
  2. computes the new totalAssets,
  3. compares it to the previous report state,
  4. finalizes profit or loss at the strategy layer.

For YDS strategies:

  • profit is represented as newly minted strategy shares sent to the donation address,
  • loss may first be absorbed by burning donation shares if enableBurning is enabled and the donation-share buffer is large enough,
  • any remaining loss reduces PPS.

If management needs to turn burn protection off while it is currently enabled, use reportAndDisableBurning() when pending dragon shares or unreported loss may exist. A direct setEnableBurning(false) can revert with report before disabling burning.

If the strategy inherits BaseHealthCheck, health checks run as an additional optional safety layer around report finalization. They are not part of the mandatory YDS lifecycle.

5. Emergency shutdown and recovery

An authorized emergency operator can shut the strategy down.

In this state:

  • new deposits and mints are blocked,
  • withdrawals and redemptions remain available,
  • emergency recovery hooks can be used to pull capital back from the external position.

_emergencyWithdraw(uint256 amount) is an optional hook that should be implemented by strategies that keep capital deployed externally. In the base architecture, this emergency path is for management or emergencyAdmin, not management alone.

Required Overrides

1. _harvestAndReport() returns (uint256 totalAssets)

Called during the trusted report() step.

This function should:

  • harvest pending rewards if your integration requires it,
  • convert any non-underlying rewards into the underlying with bounded slippage when needed,
  • return the full current asset value of the strategy, including both idle and deployed assets.

The shared implementation then compares this value to the previously reported state:

  • if there is profit, it mints donation shares,
  • if there is loss, it attempts loss absorption through the donation-share buffer when burning is enabled.

2. _deployFunds(uint256 amount)

Called after new capital becomes available to deploy.

This function should move the specified underlying amount into the external yield source. Keep it deterministic and safe for permissionless user actions.

3. _freeFunds(uint256 amount)

Called when the strategy needs to return underlying to satisfy withdrawals or redemptions.

It should attempt to free the requested amount from the external position. If the strategy cannot safely or accurately do that under certain conditions, use withdrawal limits or explicit reverts.

Optional Overrides

_tend(uint256 totalIdle) / _tendTrigger()

Optional maintenance path for strategies that need operator-driven upkeep between reports.

_emergencyWithdraw(uint256 amount)

Optional but often important for strategies that hold most of their capital in an external protocol. This should attempt to move assets back into idle balance after shutdown, with accounting finalized later through report().

availableDepositLimit(address owner)

Defaults to unlimited. Override for:

  • protocol-level caps,
  • access controls,
  • liquidity-dependent limits,
  • temporary intake throttling.

availableWithdrawLimit(address owner)

Defaults to unlimited. Override for:

  • illiquid positions,
  • cooldown-style designs,
  • upstream withdrawal constraints,
  • deliberate throttling during stressed conditions.

Optional Reporting Safety Layer: BaseHealthCheck

Strategies may inherit BaseHealthCheck to add sanity checks around report-time profit/loss.

This is optional, but often useful.

When enabled, it adds:

  • doHealthCheck
  • profitLimitRatio
  • lossLimitRatio

If a report exceeds configured bounds, the report reverts before finalization.

Example

A strategy normally earns around 2% per week. The strategist sets profitLimitRatio = 1000 (10%) and lossLimitRatio = 500 (5%). If _harvestAndReport() suddenly implies a 40% profit because of a pricing bug or accounting mistake, the report can be blocked until the issue is reviewed.

Vault Lifecycle (Strategy Aggregator)

A MultistrategyVault is a separate ERC-4626 layer that can hold multiple strategies and allocate capital across them.

Users interact with the vault through the vault's own ERC-4626 surface. Vault operators then manage:

  • which strategies are attached,
  • how much debt each strategy receives,
  • withdrawal queue behavior,
  • when strategy results are processed into vault accounting.

A vault may hold YDS strategies, compounding strategies, or a mix of both.

That means vault-level behavior and strategy-level behavior are related but different:

  • a strategy settles its own profit/loss through report(),
  • a vault separately observes and books that result through process_report(strategy).

Vault Lifecycle Summary

The lifecycle of a MultistrategyVault includes both:

  • user-facing flows such as deposits, withdrawals, mints, and redemptions,
  • operator-driven flows such as strategy addition, debt updates, report processing, and revocation.

1. Add strategies

Authorized vault operators add strategies with add_strategy(address, bool).

A newly added strategy starts with zero debt. It may optionally be inserted into the default withdrawal queue.

2. Set debt ceilings

Authorized vault operators set a maximum debt per strategy with update_max_debt_for_strategy(...).

This ceiling constrains later allocations.

3. Deposit -> idle or auto-allocation

When a user deposits into the vault:

  • the vault mints vault shares,
  • the vault's idle balance increases,
  • if autoAllocate is enabled, the vault may immediately deploy new idle assets into the first strategy in the default queue,
  • otherwise assets remain idle until an authorized debt-management action allocates them.

So a user deposit does not always imply immediate multi-strategy allocation. That depends on vault configuration.

4. Withdraw -> idle first, then queued strategy withdrawals

When a user withdraws or redeems from the vault:

  • the vault uses idle balance first,
  • if idle is insufficient, it withdraws from strategies using the applicable withdrawal queue,
  • assets are sourced from strategy positions through the vault's withdrawal path,
  • the user receives underlying from the vault.

This is important: user withdrawals are not modeled as a direct external update_debt(...) operation.

update_debt(...) is the operator-facing debt-management and rebalancing function. User withdrawal flow uses the vault's own withdrawal logic and queue system.

5. Strategy report

Strategy-level reporting is always done on the strategy itself.

A keeper or management account authorized at the strategy layer calls report() on that strategy.

For a YDS strategy, this is the moment when:

  • profit becomes donated value via minted donation shares,
  • loss may burn donation shares first if burning is enabled,
  • any residual loss reduces strategy PPS.

6. Vault process_report(strategy)

After a strategy has already reported, the vault can book that result into vault accounting by calling process_report(strategy).

This function is a vault-layer reporting step, not a strategy harvest.

It:

  • reads the vault's current position value in that strategy,
  • compares it against the vault's previously recorded value,
  • updates vault accounting for gain or loss.

At the vault layer, this call is role-gated separately from strategy keepers. In MultistrategyVault, it is tied to the vault's reporting role, not to the strategy's keeper/management permissions.

7. Rebalance with update_debt(...)

Vault operators use update_debt(strategy, targetDebt, maxLoss) to:

  • increase allocation to a strategy,
  • decrease allocation to a strategy,
  • rebalance capital between strategies,
  • preserve liquidity buffers,
  • respond to changes in performance or risk.

This is a debt-management action, not the ordinary user-withdrawal path.

8. Revoke or force revoke

Strategies can be removed cleanly with revoke_strategy(...) after debt has been withdrawn.

If a strategy cannot repay, force_revoke_strategy(...) detaches it and realizes unrepaid debt as a vault-level loss.

9. Emergency handling

Vault-level emergency controls can stop new intake and preserve operational safety.

At the strategy layer, emergency operators may also unwind positions using the strategy's own shutdown and emergency-recovery flows.

Key Point: Distinguish user flows from operator flows

A lot of confusion disappears once you separate these categories.

User flows:

  • strategy deposit / withdraw / mint / redeem,
  • vault deposit / withdraw / mint / redeem.

Strategy-operator flows:

  • report()
  • optional tend()
  • shutdown and emergency recovery.

Vault-operator flows:

  • add_strategy(...)
  • update_max_debt_for_strategy(...)
  • update_debt(...)
  • process_report(strategy)
  • revoke_strategy(...)
  • force_revoke_strategy(...).

User actions do not magically call every operator pathway. Deposits, withdrawals, reporting, and rebalancing are related, but they are distinct flows with distinct permissions.

Managing Strategies in a Vault

add_strategy(address newStrategy_, bool addToQueue_)

Adds a strategy to the vault. If addToQueue_ is true, the strategy is also inserted into the default withdrawal queue.

update_max_debt_for_strategy(address strategy_, uint256 newMaxDebt_)

Sets the maximum debt the vault may assign to that strategy.

update_debt(address strategy_, uint256 targetDebt_, uint256 maxLoss_)

Changes a strategy's assigned debt.

  • Increasing debt pushes more underlying into the strategy.
  • Decreasing debt pulls capital back toward the vault.
  • maxLoss_ bounds acceptable loss while reducing debt.

revoke_strategy(address strategy_)

Removes a strategy after the vault has fully withdrawn its debt.

force_revoke_strategy(address strategy_)

Detaches a strategy even if debt remains. Any unrepaid amount becomes a vault-level loss.

Reporting Strategy Results at the Vault Layer

process_report(address strategy_)

This function should be thought of as vault accounting synchronization, not strategy harvesting.

It is best used after a strategy has already finalized its own report() result.

For YDS strategies:

  • the vault does not mint or burn donation shares itself,
  • it only records the changed economic value of the vault's position in the already-reported strategy.

For compounding strategies:

  • the vault records the updated value of its strategy position after the strategy's own report or growth in share value.

Debt Updates and Automation

Debt management is controlled by whatever account or contract has the vault's DEBT_MANAGER authority.

That may be a dedicated debt allocator contract, but it does not have to be one specific implementation. The important thing is the role and the policy, not one hardcoded operator identity.

Debt updates must respect:

  • per-strategy maxDebt,
  • strategy deposit and withdraw limits,
  • vault liquidity requirements such as minimumTotalIdle,
  • any realized or tolerated loss constraints when reducing debt.

A strategy with losses that have not yet been properly accounted for should be handled carefully at both the strategy-reporting layer and the vault-accounting layer.