Skip to content

fix(llm): model Yearn V3 update_debt as a capped target through the batch - #397

Merged
spalen0 merged 1 commit into
mainfrom
fix/yearn-v3-update-debt-caps
Sep 29, 2026
Merged

spalen0 merged 1 commit into
mainfrom
fix/yearn-v3-update-debt-caps

Conversation

@spalen0

@spalen0 spalen0 commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator

Problem

The Yearn V3 context line for update_debt treated the target as an amount. It printed deposits (target − current_debt) and always showed the pre-batch idle.

Real case: the Cap multisig 0xb8FC49402dF3ee4f8587268FB89fda4d621a8793, nonce 252, calling the capUSDC vault 0x3Ed6aa32c930253fc990dE58fF882B9186cd0072. The batch withdraws 26.9M USDC from two Morpho strategies and sets Aave's target to 50M. The prompt said "deposits 50,000,000 USDC; idle now 0 USDC", even right after the withdrawals. The model then reported "Aave's 50,000,000 USDC target exceeds the vault's reported 26,930,230.66 USDC in assets; reconcile the simulated funding" and rated it HIGH.

That gap cannot exist. The vault's _update_debt caps a deposit by max_debt, the strategy's maxDeposit, and idle above minimum_total_idle. Its docstring says to pass a large target to allocate as much idle as possible. Aave receives the 26.93M the vault holds.

The model also said the queue reorder was inert because use_default_queue=False. That's wrong: _redeem uses the default queue unless the withdrawer passes a custom one, and the flag only decides whether custom queues are allowed.

Change

  • utils/llm/yearn_v3_batch.py (new): mirrors the vault's _update_debt (3.0.x/3.1.x) over a running copy of the vault state. Each call is described against what the earlier calls in the batch leave.
    • Withdrawals: keep minimum_total_idle and are capped by what the strategy can redeem.
    • Deposits: the target is capped by max_debt; the amount is capped by maxDeposit and by idle above minimum_total_idle.
    • Edge cases: shutdown forces a full withdrawal; a target equal to current debt reverts; inactive or revoked strategies revert.
  • utils/llm/yearn_v3_context.py:
    • Reads maxDeposit(vault) and convertToAssets(maxRedeem(vault)) for the strategies the calls name.
    • update_debt lines give the amount moved, what limited it, and vault idle before → after.
    • Ceiling and revoke lines use the running state.
    • A ceiling above the vault's size is flagged as a ceiling, not a deposit.
    • The semantics line explains that targets are not amounts and what use_default_queue really means.
  • utils/llm/README.md: documents the model.

Rendered live against the real batch:

update_debt(Morpho Steakhouse…) target_debt 0 USDC: MOVES FUNDS NOW — withdraws 26,297,013.48 USDC; … vault idle 0 USDC → 26,297,013.48 USDC (expected).
update_debt(Morpho Gauntlet…) target_debt 0 USDC: MOVES FUNDS NOW — withdraws 633,217.17 USDC; … vault idle 26,297,013.48 USDC → 26,930,230.66 USDC (expected).
update_debt(OndoHolder USDC) target_debt 1,000 USDC: MOVES FUNDS NOW — deposits 1,000 USDC; … vault idle 26,930,230.66 USDC → 26,929,230.66 USDC (expected).
update_debt(Aave V3 USDC Lender) target_debt 50,000,000 USDC: MOVES FUNDS NOW — deposits 26,929,230.66 USDC, limited by the vault's available idle (idle above minimum_total_idle); … vault idle 26,929,230.66 USDC → 0 USDC (expected).

Verification

  • uv run pytest tests/: 1611 passed, 6 skipped.
    • New tests/test_yearn_v3_batch.py covers the model: idle and minimum-idle caps, maxDeposit, withdrawals feeding later deposits, raising a withdrawal to restore minimum idle, shutdown, revoked strategies, case-insensitive addresses.
    • TestBatchDebtAccounting covers the Cap batch end to end, redeem-limited withdrawals, max_debt-limited deposits, the equal-target revert, an unregistered strategy, force-revoke after a withdrawal, and the prompt semantics.
    • test_update_debt_moves_funds_now asserted the bug itself: a 79 WETH deposit into a vault with 0 idle. It is replaced by a no-idle case and a funded case.
  • ruff format and ruff check are clean; mypy shows no errors in the two modules.

Not in scope: the report also missed what OndoHolder does. It converts USDC to Ondo rUSDY (a KYC-gated tokenized-treasury RWA), and both subscribe and redeem pass a minimum output of 0. Explaining new strategies from their source is a separate improvement.

🤖 Generated with Claude Code

…atch

The Yearn V3 context printed update_debt as "deposits target − current_debt;
idle now <pre-batch idle>". For the Cap multisig batch (nonce 252) that exits
26.9M USDC from two Morpho strategies and sets Aave's target to 50M, the
prompt said "deposits 50,000,000 USDC; idle now 0 USDC" after each step, and
the model reported a funding gap and a HIGH risk that cannot exist — the
vault deposits only the 26.93M it holds. It also misread use_default_queue
=False as "automatic use of the default queue is disabled".

- utils/llm/yearn_v3_batch.py: mirrors the vault's _update_debt over a
  running state (idle, minimum_total_idle, shutdown, per-strategy debt/max
  debt/maxDeposit/redeemable), so each call is described against what the
  earlier calls leave.
- yearn_v3_context: reads maxDeposit(vault) and convertToAssets(maxRedeem
  (vault)) for named strategies; update_debt lines show the amount moved,
  what limited it, and idle before → after; cap and revoke lines use the
  running state; the semantics line explains targets and use_default_queue.
- A ceiling above the vault's size is flagged as a ceiling, not a deposit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@spalen0
spalen0 marked this pull request as ready for review September 29, 2026 18:46
@spalen0
spalen0 merged commit 223f914 into main Sep 29, 2026
3 checks passed
@spalen0
spalen0 deleted the fix/yearn-v3-update-debt-caps branch September 29, 2026 18:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant