Found Academy useful? A $5 donation by May 14 helps us ship more, faster. Every donor counts (QF matching).

Donate

Section 10 of 18

Build
+20 Lynx

Vault: creditAvailable and debtOutstanding

What You Are Building

Two view functions that drive the vault's dynamic capital allocation. creditAvailable(strategy) answers "how much new underlying can this strategy draw on its next harvest?" debtOutstanding(strategy) answers "how much of this strategy's current debt should be returned on its next harvest?"

These are pure-view functions called by the strategy's harvest() (section 15). The strategy reads them, decides how much to send back and how much new capital to deploy, then calls vault.report() (section 11) to settle.

Why Multi-Strategy Allocation Needs These

A single-strategy vault is trivial: all idle goes to the one strategy, all strategy returns come back to the one slot. With multiple strategies, the vault needs a fair allocation policy that:

  1. Respects each strategy's debtRatio (the share of total assets it should hold).
  2. Respects the vault's overall debtRatio cap (across all strategies, summed).
  3. Doesn't drain idle below zero.
  4. Stays within per-harvest bounds (so a single harvest doesn't dump all idle into one strategy).

The math below is how that policy is encoded.

creditAvailable: Four Constraints

function creditAvailable(address strategy) public view returns (uint256) {
    if (emergencyShutdown) return 0;

    StrategyParams memory params = strategies[strategy];
    if (params.activation == 0) return 0;

    uint256 vault_TotalAssets = _totalAssets();
    uint256 vault_DebtLimit = (debtRatio * vault_TotalAssets) / MAX_BPS;
    uint256 strategy_DebtLimit = (params.debtRatio * vault_TotalAssets) / MAX_BPS;

    if (strategy_DebtLimit <= params.totalDebt || vault_DebtLimit <= totalDebt) {
        return 0;
    }

    uint256 available = strategy_DebtLimit - params.totalDebt;
    uint256 vaultAvailable = vault_DebtLimit - totalDebt;
    if (vaultAvailable < available) available = vaultAvailable;

    if (totalIdle < available) available = totalIdle;
    if (params.maxDebtPerHarvest < available) available = params.maxDebtPerHarvest;

    if (available < params.minDebtPerHarvest) return 0;

    return available;
}

Each if enforces one constraint. The binding (smallest) constraint wins.

  1. Strategy headroom: how far below its target debt is the strategy? strategy_DebtLimit - params.totalDebt.
  2. Vault headroom: how far below its total target is the vault as a whole? vault_DebtLimit - totalDebt. (Could be 0 even if strategy has headroom, happens when other strategies are over-allocated.)
  3. Idle balance: can't credit more than the vault is holding. totalIdle.
  4. Per-harvest cap: params.maxDebtPerHarvest prevents a single big credit event.

After all four, if the result is below params.minDebtPerHarvest, return 0. The strategy waits for next harvest. (Skipping a harvest because the available credit is too small to amortize gas is a real optimization in production.)

debtOutstanding: Excess to Reclaim

function debtOutstanding(address strategy) public view returns (uint256) {
    StrategyParams memory params = strategies[strategy];
    if (emergencyShutdown) return params.totalDebt;
    if (params.activation == 0) return 0;

    uint256 strategy_DebtLimit = (params.debtRatio * _totalAssets()) / MAX_BPS;
    if (params.totalDebt <= strategy_DebtLimit) return 0;
    return params.totalDebt - strategy_DebtLimit;
}

Two non-trivial cases:

  1. Emergency shutdown: return params.totalDebt. Governance wants every strategy to send everything back on its next harvest. The strategy will treat this as "I owe my entire debt and should liquidate to cover it."
  2. Normal: return params.totalDebt - strategy_DebtLimit (capped at zero). Triggered when governance reduces the strategy's debtRatio (the strategy is now over-allocated relative to its smaller target), or when the vault's _totalAssets() shrinks (loss elsewhere, every strategy is now over-allocated relative to a smaller pie).

A Worked Example

Vault with _totalAssets() = $10M, debtRatio = 9_000 (vault wants 90% deployed), totalDebt = $7M. Three strategies:

StrategydebtRatio (BPS)totalDebtminDebtPerHarvestmaxDebtPerHarvest
A4_000$4M$0$1M
B3_000$2M$100k$500k
C2_000$1M$0$2M

Strategy A's creditAvailable:

  • strategy_DebtLimit = 4000 * 10M / 10000 = $4M. totalDebt = $4M. Headroom = 0. Returns 0.

Strategy B's creditAvailable:

  • strategy_DebtLimit = 3000 * 10M / 10000 = $3M. totalDebt = $2M. Strategy headroom = $1M.
  • vault_DebtLimit = 9000 * 10M / 10000 = $9M. totalDebt = $7M. Vault headroom = $2M.
  • min(strategy, vault) = $1M.
  • totalIdle = $10M - $7M = $3M. Bounded to $1M (already smaller).
  • maxDebtPerHarvest = $500k. Bounded to $500k.
  • minDebtPerHarvest = $100k. $500k > $100k, returns $500k. Returns $500k.

Strategy C's creditAvailable:

  • strategy_DebtLimit = 2000 * 10M / 10000 = $2M. totalDebt = $1M. Strategy headroom = $1M.
  • Vault headroom (still) $2M.
  • min(strategy, vault) = $1M.
  • totalIdle = $3M. Bounded.
  • maxDebtPerHarvest = $2M. Bounded ($1M, already smaller).
  • minDebtPerHarvest = $0. Returns $1M. Returns $1M.

Strategy A's debtOutstanding:

  • strategy_DebtLimit = $4M = totalDebt. Returns 0.

If governance reduces strategy A's debtRatio to 2_000:

  • strategy_DebtLimit = 2000 * 10M / 10000 = $2M. totalDebt = $4M. Returns $4M - $2M = $2M outstanding.

A is now expected to return $2M on its next harvest. Section 11's report() will settle this.

What's Next

Section 11 builds report(), the function the strategy calls to acknowledge gain/loss and settle the credit/debt swap computed here.

Your Code

Solution.sol
Solidity
Loading editor...

Requirements

Write your implementation, then click Run Tests. Tests execute on the server.