Section 11 of 18
Vault: The report() Cycle (gain, loss, debt netting)
What You Are Building
The most important function in the vault. report() is called by every strategy on every harvest. It validates the strategy's claim, applies any loss, assesses fees, computes the new credit/debt allocation, and executes the net underlying transfer between vault and strategy.
If you understand report(), you understand multi-strategy vaults.
The Seven Steps
function report(uint256 gain, uint256 loss, uint256 _debtPayment)
external nonReentrant returns (uint256 debtOutstanding_)
{
// 1. Validate caller is a registered strategy.
require(strategies[msg.sender].activation > 0, "Vault/unknown-strategy");
// 2. Validate the gain claim: strategy must HOLD the gain right now.
require(
IERC20(token).balanceOf(msg.sender) >= gain + _debtPayment,
"Vault/incorrect-balance"
);
// 3. Loss-first accounting.
if (loss > 0) {
_reportLoss(msg.sender, loss);
}
// 4. Fees on gain (section 16 fills this in).
uint256 totalFees = _assessFees(msg.sender, gain);
strategies[msg.sender].totalGain += gain;
// 5. Credit/debt computation.
uint256 credit = creditAvailable(msg.sender);
uint256 outstanding = debtOutstanding(msg.sender);
debtOutstanding_ = outstanding;
uint256 actualPayment = _debtPayment > outstanding ? outstanding : _debtPayment;
if (actualPayment > 0) {
strategies[msg.sender].totalDebt -= actualPayment;
totalDebt -= actualPayment;
debtOutstanding_ -= actualPayment;
}
// 6. Net underlying transfer.
uint256 strategyOwes = gain + actualPayment;
if (strategyOwes >= credit) {
uint256 inflow = strategyOwes - credit;
if (inflow > 0) {
require(
IERC20(token).transferFrom(msg.sender, address(this), inflow),
"Vault/strategy-payment-failed"
);
totalIdle += inflow;
}
} else {
uint256 outflow = credit - strategyOwes;
require(
IERC20(token).transfer(msg.sender, outflow),
"Vault/credit-transfer-failed"
);
totalIdle -= outflow;
}
if (credit > 0) {
strategies[msg.sender].totalDebt += credit;
totalDebt += credit;
}
// 7. Lock the freshly-reported profit (section 13 fills this in).
_updateLockedProfit(gain, totalFees);
strategies[msg.sender].lastReport = block.timestamp;
lastReport = block.timestamp;
emit StrategyReported(...);
}
Each step matters. Walk through them slowly.
The Anti-Fake-Gain Check
Step 2 is the load-bearing one for security:
require(
IERC20(token).balanceOf(msg.sender) >= gain + _debtPayment,
"Vault/incorrect-balance"
);
Without this, a malicious or buggy strategy could call report(gain=$1M, loss=0, _debtPayment=0) and the vault would happily mint share-tokens to the rewards/strategist as if a million was earned, then NOT actually receive the underlying. The check forces the strategy to physically hold what it's claiming.
This is the line that prevents most "strategy reporting bug" exploits. Reaper Farm's August 2022 incident lived on a different surface (a recipient-verification gap in the withdraw flow rather than a report bug), but the same defensive principle applies across both: at the moment of any state-changing call from a strategy, the underlying token balance is the source of truth, and the vault must verify that the strategy's claim matches reality.
Loss-First Accounting
function _reportLoss(address strategy, uint256 loss) internal {
StrategyParams storage params = strategies[strategy];
require(loss <= params.totalDebt, "Vault/loss-exceeds-debt");
if (params.debtRatio > 0 && params.totalDebt > 0) {
uint256 ratioChange = (loss * params.debtRatio) / params.totalDebt;
if (ratioChange > params.debtRatio) ratioChange = params.debtRatio;
params.debtRatio -= ratioChange;
debtRatio -= ratioChange;
}
params.totalLoss += loss;
params.totalDebt -= loss;
totalDebt -= loss;
}
Yearn's automatic risk de-allocation: a strategy that loses 10% of its debt has its debtRatio cut by 10% as well. The vault doesn't need governance to react to a bad strategy; the next harvest will give it less capital because its target shrunk.
The proportional formula loss * params.debtRatio / params.totalDebt is (loss / totalDebt) * debtRatio rearranged for integer arithmetic. The reorder matters: loss * debtRatio first preserves precision; doing the division first would round down to zero for small losses and fail to cut the ratio.
The Net Transfer
Step 6 is the only step that actually moves underlying tokens. Strategy owes the vault gain + actualPayment (profit from the report + return of any debt they're paying down). Vault owes the strategy credit (new capital allocation). Net it. Whoever owes more transfers the difference.
The conditional split keeps the function symmetric: it works whether the strategy is paying down or drawing more, without two separate code paths. Same report() call handles both directions.
The Fee and LockedProfit Stubs
Steps 4 and 7 call _assessFees and _updateLockedProfit. Both are declared in this section as internal virtual with naive default bodies (return 0 / lock the gross gain). Section 13 (locked profit) overrides _updateLockedProfit with proper time-decay; section 16 (fees) overrides _assessFees with the management+performance fee math. The override pattern keeps report() itself stable while the financial policy evolves.
Why _assessFees Goes Before Credit/Debt Math
Subtle ordering point. Fees are minted as new shares (section 16). Minting changes totalSupply. The credit/debt math doesn't depend on totalSupply, only on _totalAssets() (which depends on totalIdle + totalDebt, not totalSupply). So the order doesn't affect the math here.
It DOES affect the math in subsequent calls, once new fee shares exist, future pricePerShare reads and _issueSharesForAmount calls reflect them. That is the intended dilution effect.
Bug History
The strategy reporting layer is where multiple Yearn forks have been exploited. Pattern: a subclass of BaseStrategy implements _prepareReturn incorrectly, the vault accepts the bad numbers, the loss is realized later. The vault's defenses (balance check, loss accounting, fee cap) limit the damage but don't prevent it entirely, strategist code quality matters.
Reaper Farm (August 2022, $1.7M) is the closest historical reference, although the specific bug was in withdraw rather than report (a recipient-verification gap that let an attacker destroy other users' shares and withdraw the underlying). The defensive principle is shared: vault state-validation gaps on calls from strategy code are the recurring exploit pattern.
What's Next
Section 12 is the MVP checkpoint, a final-build that verifies sections 2-11 integrate correctly. Then Part 3 begins: locked profit (section 13), withdraw (section 14), strategy harvest (section 15), fees (section 16), emergency + roles (section 17).