feat(watchdog): layer-0 pre-rescue Morpho vault withdrawal#40
Merged
Conversation
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>
Contributor
There was a problem hiding this comment.
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
MorphoVaultWithdrawV1helper 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.
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 crossestriggerHF, 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
triggerHFcheck.Approach
1. New on-chain helper:
MorphoVaultWithdrawV1.solNew contract at
packages/rescue-contract/src/MorphoVaultWithdrawV1.solmodeled onMorphoAtomicRepayV1:owner= monitored wallet,executor= bot hot wallet).setSupportedVault(address vault, bool enabled)allowlist, mirroringsetSupportedMarket.withdraw(WithdrawParams { user, vault, assets, deadline }), callable only byexecutor:user == owner, deadline, supported vault, non-zero assets.IERC4626(vault).withdraw(assets, user, user)— Morpho MetaMorpho vaults are ERC-4626 compliant, so this redeems shares fromuserand sends underlying assets straight touser's wallet. The contract itself never custodies the funds.previewMaxWithdraw(vault, user) view returns (uint256)thin wrapper overIERC4626.maxWithdrawfor off-chain capping.Owner pre-approves the helper on vault shares (
vault.approve(helper, type(uint256).max)). The executor key only needs to callwithdraw(...); it never touches vault shares directly. A single deployed instance per monitored wallet supports many vaults viasetSupportedVault.2. Config additions (
WatchdogConfig)Defaults hydrated in
constants.ts/storage.ts; env overridesWATCHDOG_PRE_TRIGGER_HF,WATCHDOG_VAULT_WITHDRAW_CONTRACT,WATCHDOG_MAX_VAULT_WITHDRAW_AMOUNT; zod schema updated; dashboardServerSettingsform gains four new controls in a dedicated "Pre-rescue" subsection with cross-field validation.3. Watchdog evaluation flow
Watchdog.evaluatenow acceptsvaults: MorphoVaultPosition[]andmonitor.tspasses them through.A new step runs before the layer-1
triggerHFearly return:!preRescueEnabledor HF outside buffer band → fall through.${wallet}-${loan}-prerescue).previewFn+findRequiredAmountRawGenericto computeneededRawcapped atmaxVaultWithdrawAmount.neededRaw, skip.asset.address == debtToken, pick the entry with the largesttotalAssets. If none →skipped.withdrawRaw = min(shortfall, vaultAssets, maxVaultWithdraw).dryRun(log + Telegram preview) or submitMorphoVaultWithdrawV1.withdraw(...)signed by executor.vault-withdraw/vault-withdraw-dry-runlog actions and a new optionalvaultAddressfield. Send a Telegram notice.getStatusSummary()now also surfacespreRescueEnabled,preRescueTriggerHF, andvaultWithdrawContractso/watchdogTelegram +GET /api/watchdog/statusreflect layer-0 state.4. Tests
Eight new tests in
packages/server/test/watchdog.test.tscovering:preRescueTriggerHF→ no action.withdraw(live + dry-run paths).skippedlog entry.triggerHF→ layer 0 bypassed, layer 1 runs as before (regression).preRescueEnabled→ existing behavior unchanged.maxVaultWithdrawAmountcap respected.5. Files
packages/rescue-contract/src/MorphoVaultWithdrawV1.sol(new)packages/aave-core/src/types.ts,constants.tspackages/server/src/storage.ts,configSchema.ts,watchdog.ts,monitor.tssrc/components/ServerSettings.tsxpackages/server/test/watchdog.test.tsdocs/watchdog-user-manual.mdTest plan
yarn typecheck— passesyarn lint— passesyarn format:check— passesyarn test— 123/123 pass (8 new layer-0 tests + all existing tests untouched)MorphoVaultWithdrawV1to a fork,setSupportedVaultfor a USDC vault, approve vault shares, setpreRescueEnabled: true+dryRun: true, force a low HF and verify avault-withdraw-dry-runentry appears in/api/watchdog/status+ TelegrammaxVaultWithdrawAmount: confirm executor tx redeems vault shares, USDC arrives in monitored wallet, subsequent layer-1 rescue (if triggered) consumes itpreRescueEnabled: false— existing watchdog flows unaffected🤖 Generated with Claude Code