Skip to content

feat(watchdog): layer-0 pre-rescue Morpho vault withdrawal#40

Merged
felipecsl merged 4 commits into
masterfrom
watchdog-layer-0-vault-withdraw
May 29, 2026
Merged

feat(watchdog): layer-0 pre-rescue Morpho vault withdrawal#40
felipecsl merged 4 commits into
masterfrom
watchdog-layer-0-vault-withdraw

Conversation

@felipecsl

Copy link
Copy Markdown
Member

Context

Today the watchdog can only repay debt using debt-token balance already sitting in the monitored wallet (layer 1). If those funds are deployed into a Morpho ERC-4626 vault (e.g. Gauntlet USDC Prime), the watchdog will log "No available USDC" and skip the rescue even though the operator has plenty of capital to defend the loan.

This PR adds a pre-rescue stage (layer 0) that fires before HF reaches the layer-1 trigger. When HF is in the buffer band [triggerHF, preRescueTriggerHF), the watchdog looks for a Morpho vault holding the loan's debt token and pre-emptively redeems just enough vault shares to cover the projected rescue. The redeemed assets land in the monitored wallet. If HF later crosses triggerHF, the existing layer-1 rescue uses those wallet funds with no change to its code path. If HF recovers, the funds simply remain in the wallet for the operator to redeposit — the layer-0 withdrawal is intentionally one-way and non-atomic with rescue.

The existing rescue contracts and the layer-1 flow are not modified. Layer 0 is a strictly additive step that runs before the existing triggerHF check.

Approach

1. New on-chain helper: MorphoVaultWithdrawV1.sol

New contract at packages/rescue-contract/src/MorphoVaultWithdrawV1.sol modeled on MorphoAtomicRepayV1:

  • Same owner / executor split (owner = monitored wallet, executor = bot hot wallet).
  • setSupportedVault(address vault, bool enabled) allowlist, mirroring setSupportedMarket.
  • withdraw(WithdrawParams { user, vault, assets, deadline }), callable only by executor:
    • Validates user == owner, deadline, supported vault, non-zero assets.
    • Calls IERC4626(vault).withdraw(assets, user, user) — Morpho MetaMorpho vaults are ERC-4626 compliant, so this redeems shares from user and sends underlying assets straight to user's wallet. The contract itself never custodies the funds.
  • previewMaxWithdraw(vault, user) view returns (uint256) thin wrapper over IERC4626.maxWithdraw for off-chain capping.

Owner pre-approves the helper on vault shares (vault.approve(helper, type(uint256).max)). The executor key only needs to call withdraw(...); it never touches vault shares directly. A single deployed instance per monitored wallet supports many vaults via setSupportedVault.

2. Config additions (WatchdogConfig)

preRescueEnabled: boolean;          // master switch for layer 0
preRescueTriggerHF: number;          // default 1.7
vaultWithdrawContract: string;       // address of MorphoVaultWithdrawV1
maxVaultWithdrawAmount: number;      // per-loan, debt-token denominated cap

Defaults hydrated in constants.ts/storage.ts; env overrides WATCHDOG_PRE_TRIGGER_HF, WATCHDOG_VAULT_WITHDRAW_CONTRACT, WATCHDOG_MAX_VAULT_WITHDRAW_AMOUNT; zod schema updated; dashboard ServerSettings form gains four new controls in a dedicated "Pre-rescue" subsection with cross-field validation.

3. Watchdog evaluation flow

Watchdog.evaluate now accepts vaults: MorphoVaultPosition[] and monitor.ts passes them through.

A new step runs before the layer-1 triggerHF early return:

  1. If !preRescueEnabled or HF outside buffer band → fall through.
  2. Separate cooldown key (${wallet}-${loan}-prerescue).
  3. Reuse existing previewFn + findRequiredAmountRawGeneric to compute neededRaw capped at maxVaultWithdrawAmount.
  4. Read wallet debt-token balance. If wallet already covers neededRaw, skip.
  5. Pick the source vault: filter to asset.address == debtToken, pick the entry with the largest totalAssets. If none → skipped.
  6. Compute withdrawRaw = min(shortfall, vaultAssets, maxVaultWithdraw).
  7. Honor dryRun (log + Telegram preview) or submit MorphoVaultWithdrawV1.withdraw(...) signed by executor.
  8. Record outcome with new vault-withdraw / vault-withdraw-dry-run log actions and a new optional vaultAddress field. Send a Telegram notice.
  9. Set the prerescue cooldown timestamp; return without invoking layer 1 this poll.

getStatusSummary() now also surfaces preRescueEnabled, preRescueTriggerHF, and vaultWithdrawContract so /watchdog Telegram + GET /api/watchdog/status reflect layer-0 state.

4. Tests

Eight new tests in packages/server/test/watchdog.test.ts covering:

  • HF above preRescueTriggerHF → no action.
  • HF in buffer band, wallet already funded → skip.
  • HF in buffer band, vault available → submits withdraw (live + dry-run paths).
  • HF in buffer band, no matching vault → skipped log entry.
  • HF below triggerHF → layer 0 bypassed, layer 1 runs as before (regression).
  • Disabled preRescueEnabled → existing behavior unchanged.
  • maxVaultWithdrawAmount cap respected.

5. Files

  • packages/rescue-contract/src/MorphoVaultWithdrawV1.sol (new)
  • packages/aave-core/src/types.ts, constants.ts
  • packages/server/src/storage.ts, configSchema.ts, watchdog.ts, monitor.ts
  • src/components/ServerSettings.tsx
  • packages/server/test/watchdog.test.ts
  • docs/watchdog-user-manual.md

Test plan

  • yarn typecheck — passes
  • yarn lint — passes
  • yarn format:check — passes
  • yarn test — 123/123 pass (8 new layer-0 tests + all existing tests untouched)
  • Deploy MorphoVaultWithdrawV1 to a fork, setSupportedVault for a USDC vault, approve vault shares, set preRescueEnabled: true + dryRun: true, force a low HF and verify a vault-withdraw-dry-run entry appears in /api/watchdog/status + Telegram
  • Live smoke test on mainnet with tiny maxVaultWithdrawAmount: confirm executor tx redeems vault shares, USDC arrives in monitored wallet, subsequent layer-1 rescue (if triggered) consumes it
  • Regression check with preRescueEnabled: false — existing watchdog flows unaffected

🤖 Generated with Claude Code

felipecsl and others added 2 commits May 28, 2026 13:31
When debt-token funds are parked in a Morpho ERC-4626 vault, the existing
rescue would skip with "no available debt token" even though plenty of
capital is available. Layer 0 fires opportunistically in the buffer band
[triggerHF, preRescueTriggerHF), withdraws the shortfall from a matching
Morpho vault into the monitored wallet, and lets the existing layer-1
rescue consume it on the next poll if HF crosses triggerHF. The existing
rescue code path is unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Compute repay capacity as min(wallet + maxVaultWithdraw, maxRepay) so
  needed amount is searched over the full wallet+vault capacity, not just
  maxVaultWithdraw. Fixes scenario where wallet 400 + vault 500 (cap=500)
  was wrongly skipped because 500 alone could not reach target HF.
- Query ERC-4626 maxWithdraw on-chain per matching vault and pick the vault
  with the largest user-withdrawable amount (Morpho API totalAssets is not
  user-specific). withdrawRaw is now capped by maxWithdraw, preventing
  doomed reverts that would otherwise consume the pre-rescue cooldown.
- Extend server-side validateWatchdogThresholds with the same cross-field
  rules the UI enforces: preRescueTriggerHF > triggerHF, valid
  vaultWithdrawContract address, preRescueEnabled requires a valid helper.
- Surface layer-0 fields in /watchdog Telegram status (preRescue state,
  preRescueTriggerHF, vaultWithdrawContract).
- Add 12 Forge tests for MorphoVaultWithdrawV1 covering owner/executor
  authz, supported-vault gating, deadline, user==owner check, ERC-4626
  happy path, executor rotation, and vault-revert propagation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a layer-0 watchdog pre-rescue flow that can withdraw debt-token liquidity from Morpho ERC-4626 vaults into the monitored wallet before the existing layer-1 rescue trigger is reached.

Changes:

  • Adds MorphoVaultWithdrawV1 helper contract and Foundry tests.
  • Extends watchdog config, status, validation, UI settings, and documentation for pre-rescue.
  • Updates watchdog evaluation to inspect Morpho vault positions and submit/dry-run vault withdrawals.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/rescue-contract/src/MorphoVaultWithdrawV1.sol Adds ERC-4626 vault withdrawal helper contract.
packages/rescue-contract/test/MorphoVaultWithdrawV1.t.sol Adds contract tests for helper permissions and withdrawal behavior.
packages/server/src/watchdog.ts Implements layer-0 pre-rescue vault withdrawal logic and logging.
packages/server/src/monitor.ts Passes Morpho vault positions into watchdog evaluation.
packages/server/src/storage.ts Adds environment/default hydration for new watchdog settings.
packages/server/src/configSchema.ts Extends config schema with pre-rescue fields.
packages/server/src/runtime.ts Adds server validation and status output for pre-rescue.
packages/server/test/watchdog.test.ts Adds watchdog tests for pre-rescue paths and caps.
packages/aave-core/src/types.ts Extends shared watchdog config type.
packages/aave-core/src/constants.ts Adds default pre-rescue watchdog config values.
src/components/ServerSettings.tsx Adds UI controls and client validation for pre-rescue settings.
docs/watchdog-user-manual.md Documents layer-0 pre-rescue behavior and setup.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/components/ServerSettings.tsx Outdated
Comment thread packages/server/src/runtime.ts Outdated
felipecsl and others added 2 commits May 28, 2026 18:35
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@felipecsl felipecsl merged commit 4b5aab2 into master May 29, 2026
1 check passed
@felipecsl felipecsl deleted the watchdog-layer-0-vault-withdraw branch May 29, 2026 01:39
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.

2 participants