M-1: Negative equity harvest blocks strategy deposits
Summary:
When _harvestAndReport reports totalAssets = 0 (negative equity exceeding idle balance), TokenizedStrategy blocks all subsequent deposits with a ZERO_SHARES revert. Since the Yearn V3 Vault uses strategy.deposit() via update_debt to allocate funds to strategies, this means the vault cannot push new debt to the affected strategy. This could break the 95/5 allocation ratio and leave user funds sitting idle in the vault, undeployed and earning nothing.
Description:
In RISExStrategy._harvestAndReport(), when equity is negative and exceeds idle:
function _harvestAndReport() internal override returns (uint256 totalAssets_) {
uint256 idle = ERC20(asset).balanceOf(address(this));
int256 equity = getAccountEquity();
if (equity >= 0) {
totalAssets_ = idle + uint256(equity);
} else {
uint256 loss = uint256(-equity);
totalAssets_ = idle > loss ? idle - loss : 0;
}
_resetEpoch(totalAssets_.toUint128(), idle.toUint128());
}
When totalAssets = 0 but totalSupply > 0 (existing shares), any call to strategy.deposit() computes 0 shares for the depositor, and TokenizedStrategy reverts with ZERO_SHARES to protect the caller.
The cascade:
- Strategy equity goes negative (positions underwater) → harvest reports
totalAssets = 0 vault.update_debt(strategy, target, 0)callsstrategy.deposit()→ revertsZERO_SHARES- Vault cannot allocate debt to this strategy → funds sit as vault idle
- The 95/5 ratio between MM and RLP strategies cannot be maintained
- If MM is the blocked strategy, 95% of new deposits remain idle
- If RLP is blocked, liquidation capacity shrinks (caps are TVL-based)
PoC
contract PoC_NegativeEquityBlocksDeposits is RISExSetup {
address public vaultDepositor = address(20);
function setUp() public override {
super.setUp();
whitelistDepositor(vaultDepositor);
}
/// @notice After harvest with totalAssets=0, strategy blocks ALL deposits (including vault's update_debt)
function test_PoC_negativeEquity_strategyBlocksDepositsAfterHarvest() public {
// 1. User deposits 100k, harvest healthy
deposit(user, 100_000e6);
mockAccountEquity(int256(100_000e18));
mockWithdrawableAmount(100_000e18);
mockFreeCollateral(100_000e6);
report();
assertGt(iStrategy.totalAssets(), 0, "precondition: totalAssets > 0");
// 2. Equity goes deeply negative: -120k, idle = 0
// loss(120k) > idle(0) → totalAssets = 0
mockAccountEquity(int256(-120_000e18));
mockWithdrawableAmount(0);
// 3. Harvest reports totalAssets = 0
report();
assertEq(iStrategy.totalAssets(), 0, "totalAssets is 0 after negative equity harvest");
assertGt(iStrategy.totalSupply(), 0, "shares still exist");
// 4. Strategy blocks deposits — ZERO_SHARES revert
// In production this is vault calling strategy.deposit() via update_debt
// Same code path, same revert
deal(address(asset), vaultDepositor, 50_000e6);
vm.startPrank(vaultDepositor);
asset.approve(address(strategy), 50_000e6);
vm.expectRevert("ZERO_SHARES");
iStrategy.deposit(50_000e6, vaultDepositor);
vm.stopPrank();
// CONFIRMED: strategy is frozen for new deposits until equity recovers + harvest
}
}
Impact:
Medium.
- Vault debt allocation blocked:
update_debtto the affected strategy reverts, breaking the 95/5 ratio - Capital inefficiency: User deposits into the vault succeed but funds sit idle, earning nothing
- RLP liquidation capacity: If RLP is blocked, the protocol cannot take over liquidated positions (daily caps scale with TVL which is now 0)
- Recovery requires two steps: equity must go positive AND a harvest must run — if the adapter is also down, recovery is impossible
- Pre-harvest gap: Between crash and harvest, deposits are accepted into a bankrupt account (stale totalAssets)
User deposits into the VaultV3 itself are not blocked — only the vault's ability to allocate those funds to strategies.
Recommendation:
Remove the bankrupt strategy for the vault and replace it with a new strategy of the same type. This can be done in a single transaction, call report with asset = 0, remove bankrupt strategy, and add a new strategy to the vault at the end of the queue.
Developer Response:
Acknowledge.