Section 9 of 18
Vault: addStrategy, revokeStrategy, withdrawalQueue
What You Are Building
The strategy registry. Governance adds strategies via addStrategy(), sets per-strategy parameters (debtRatio, minDebtPerHarvest, maxDebtPerHarvest, performanceFee), and orders them in the withdrawalQueue. Sections 10 and 11 then drive capital allocation between them.
This section also includes initVault(), the one-time initializer that seeds token, share-token metadata, and roles. Section 17 will wrap this in the deployable Vault's actual constructor; for testing sections 9-16 in isolation, the test harness calls initVault once after deploying the bare contract.
The Registry Data Model
Strategies live in two places in storage (declared back in section 2):
mapping(address => StrategyParams) public strategies;
address[MAXIMUM_STRATEGIES] public withdrawalQueue;
The mapping holds per-strategy params. The array is the order in which the vault drains strategies on user withdraw. A strategy can be in the mapping but absent from the queue, or in both. The two are managed independently.
Why MAXIMUM_STRATEGIES = 20? Because withdraw() (section 14) iterates the queue. A bounded loop guarantees gas cost is constant per call. Yearn V2 picked 20 as the cap; we keep it.
addStrategy: All The Required Checks
function addStrategy(
address strategy,
uint256 _debtRatio,
uint256 _minDebtPerHarvest,
uint256 _maxDebtPerHarvest,
uint256 _performanceFee
) external onlyGovernance {
require(strategy != address(0), "Vault/zero-strategy");
require(strategies[strategy].activation == 0, "Vault/strategy-active");
require(IStrategy(strategy).vault() == address(this), "Vault/wrong-vault");
require(IStrategy(strategy).want() == address(token), "Vault/wrong-want");
require(debtRatio + _debtRatio <= MAX_BPS, "Vault/debt-ratio-exceeded");
require(_minDebtPerHarvest <= _maxDebtPerHarvest, "Vault/bad-debt-bounds");
require(_performanceFee <= MAX_BPS / 2, "Vault/perf-fee-too-high");
...
}
Six require checks before any state change. The first three reject malformed input (zero address, already registered, wrong vault). The fourth and fifth catch governance misconfiguration (a strategy whose want() mismatches the vault's token would silently accept funds in the wrong asset). The sixth and seventh enforce policy bounds.
The IStrategy(strategy).vault() check is the boundary between vault and strategy chains, first time we use the interface from section 7. Validates that the strategy was constructed pointing at THIS vault, not someone else's.
Updating Per-Strategy Params
function updateStrategyDebtRatio(address strategy, uint256 _debtRatio)
external onlyGovernanceOrManagement
{
require(strategies[strategy].activation > 0, "Vault/strategy-not-active");
debtRatio = debtRatio - strategies[strategy].debtRatio + _debtRatio;
require(debtRatio <= MAX_BPS, "Vault/debt-ratio-exceeded");
strategies[strategy].debtRatio = _debtRatio;
}
The cumulative debtRatio (vault-wide sum) is maintained on every per-strategy change. Computing the new cumulative requires both subtracting the old and adding the new in one expression to avoid an intermediate underflow if the old was higher than the new.
updateStrategyMinDebtPerHarvest, updateStrategyMaxDebtPerHarvest, and updateStrategyPerformanceFee follow the same pattern: validate the strategy is active, validate the new bound, set it.
Withdrawal Queue Management
The queue is a fixed-size array. Three operations:
setWithdrawalQueue(addr[20] queue): replace wholesale. Used to reorder.addStrategyToQueue(address): append to the first empty slot.removeStrategyFromQueue(address): find the strategy and shift everything after it left by one.
The "no holes" invariant matters because section 14's withdraw() iterates until it hits address(0), a hole would short-circuit early and leave strategies after the hole untouched. setWithdrawalQueue enforces this by rejecting any non-zero entry after the first zero.
Why This Section Has initVault
Sections 9-16 inherit the Vault chain but don't have constructors. The deployable contract's constructor lives in section 17. To test sections 9-16 in isolation (deploy directly, exercise functions), you need a way to set token, name, symbol, and roles. That's initVault.
function initVault(
address token_,
string memory name_,
string memory symbol_,
address governance_,
address management_,
address guardian_,
address rewards_
) public {
require(activation == 0, "Vault/already-initialized");
...
}
Idempotency is enforced by the activation == 0 check. Once initVault runs, it sets activation = block.timestamp (also used by management-fee accrual in section 16), so a second call reverts.
In production, the section 17 constructor calls initVault exactly once. In tests, the deployer calls it. The function is public not external so the constructor in section 17 can call it as a regular method.
What's Next
Section 10 builds creditAvailable() and debtOutstanding(), the dynamic capital allocation math that uses the registry built here. Section 11 builds report(), the central function strategies call.