feat: move watchdog to atomic on-chain rescue v1 (breaking)#17
feat: move watchdog to atomic on-chain rescue v1 (breaking)#17
Conversation
…rovider On Aave v3 the DataProvider is read-only; setUserUseReserveAsCollateral lives on the Pool contract. The previous code would revert on mainnet. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…safety revert tests Wrap live rescue submission in try/catch so a failed tx logs the error, sets cooldown, and notifies instead of propagating and causing immediate retries. Add Solidity tests for ResultingHFTooLow and AssetNotSupported revert paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
HF is linear in top-up amount, so two previewResultingHF calls (parallel) plus one verification call suffice — down from ~26 sequential RPC calls. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…for another user Aave v3's setUserUseReserveAsCollateral operates on msg.sender, so the rescue contract was toggling the flag on its own position, not the user's. Remove the broken call and document the precondition: the user must have WBTC enabled as collateral before activating live rescue mode. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR replaces the multi-transaction repay-based watchdog flow with an atomic on-chain rescue system (v1). The watchdog now computes a WBTC collateral top-up amount off-chain and submits a single rescue(...) transaction to a dedicated Solidity contract that atomically supplies collateral and verifies the resulting health factor.
Changes:
- Added
AaveAtomicRescueV1Solidity contract with Foundry tests and deploy script for atomic WBTC collateral top-up rescue - Migrated watchdog config, schema, UI, and server logic from repay-specific fields (
maxRepayUsd) to rescue-specific fields (minResultingHF,maxTopUpWbtc,deadlineSeconds,rescueContract) - Updated documentation, ops runbook, CI pipeline, and tests to reflect the new rescue v1 behavior
Reviewed changes
Copilot reviewed 27 out of 30 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/rescue-contract/src/AaveAtomicRescueV1.sol | New Solidity contract for atomic Aave rescue via collateral supply |
| packages/rescue-contract/test/AaveAtomicRescueV1.t.sol | Foundry unit tests for rescue contract |
| packages/rescue-contract/script/DeployAaveAtomicRescueV1.s.sol | Foundry deploy script |
| packages/rescue-contract/foundry.toml | Foundry project config |
| packages/rescue-contract/foundry.lock | Foundry dependency lock |
| packages/rescue-contract/README.md | Package documentation |
| packages/rescue-contract/lib/forge-std | forge-std submodule |
| packages/server/src/watchdog.ts | Replaced repay logic with atomic rescue planner/submitter |
| packages/server/src/storage.ts | Added env overrides for new watchdog fields |
| packages/server/src/runtime.ts | Added validation and formatting for new fields |
| packages/server/src/monitor.ts | Removed walletStablecoinBalances from evaluate call |
| packages/server/src/index.ts | Updated Zod schema for new watchdog fields |
| packages/server/test/watchdog.test.ts | Rewrote tests for rescue flow |
| packages/server/test/storage.test.ts | Updated tests for new config fields |
| packages/server/test/runtime.test.ts | Updated tests for new validation and formatting |
| packages/aave-core/src/types.ts | Added new watchdog config type fields |
| packages/aave-core/src/metrics.ts | Removed computeRepaymentAmount |
| packages/aave-core/src/index.ts | Removed export of computeRepaymentAmount |
| packages/aave-core/src/constants.ts | Updated default watchdog config values |
| src/components/ServerSettings.tsx | Added UI controls for new rescue config fields |
| docs/watchdog-user-manual.md | Rewrote for rescue v1 |
| docs/rescue-v1-ops.md | New ops runbook |
| plans/rescue-v1.md | Design plan document |
| CLAUDE.md | Updated agent instructions |
| README.md | Updated project docs |
| package.json | Added test:contracts and deploy scripts |
| .github/workflows/ci.yml | Added Foundry setup and contract tests |
| .gitmodules | Added forge-std submodule |
| .gitignore | Added Foundry build artifacts |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| bool resetOk = IERC20(asset).approve(spender, 0); | ||
| if (!resetOk) revert TokenApproveFailed(); | ||
| } | ||
| bool ok = IERC20(asset).approve(spender, amount); |
There was a problem hiding this comment.
_forceApprove approves only the exact amount needed for the current rescue. If there's any token dust left after pool.supply (e.g., due to rounding), a stale non-zero approval will remain for the Aave pool. More importantly, approving only the exact amount means every rescue call will re-approve. Consider approving type(uint256).max once (as is common for Aave interactions) or explicitly resetting to 0 after the supply call to avoid dangling approvals.
| bool ok = IERC20(asset).approve(spender, amount); | |
| bool ok = IERC20(asset).approve(spender, type(uint256).max); |
packages/server/src/watchdog.ts
Outdated
| } | ||
|
|
||
| private toWad(value: number): bigint { | ||
| return parseUnits(value.toFixed(18), 18); |
There was a problem hiding this comment.
value.toFixed(18) can produce unexpected results for values like 1.85 due to floating-point representation. For example, (1.85).toFixed(18) yields "1.849999999999999956", which when parsed to 18 decimal wad will be slightly less than the intended 1.85e18. This could cause the projectedHFWad < minHFWad comparison to produce incorrect results (e.g., treating a HF of exactly 1.85 as below the minimum). Consider using a string-based approach or rounding to fewer significant decimals (e.g., value.toFixed(4)) before parsing.
| return parseUnits(value.toFixed(18), 18); | |
| // Round to 4 decimal places to avoid floating-point artifacts like 1.849999999999999956 | |
| return parseUnits(value.toFixed(4), 18); |
| if (minResultingHF !== undefined) watchdog.minResultingHF = minResultingHF; | ||
|
|
||
| const maxTopUpWbtc = parseEnvFloat('WATCHDOG_MAX_TOP_UP_WBTC'); | ||
| if (maxTopUpWbtc !== undefined) watchdog.maxTopUpWbtc = maxTopUpWbtc; |
There was a problem hiding this comment.
The new env overrides for WATCHDOG_MIN_RESULTING_HF and WATCHDOG_MAX_TOP_UP_WBTC are added, but WATCHDOG_DEADLINE_SECONDS and WATCHDOG_RESCUE_CONTRACT (referenced as rescueContract in the config) are not present as env overrides, while the docs in CLAUDE.md only mention the two added here. Consider whether deadlineSeconds and rescueContract should also have env overrides for consistency, or document why they are intentionally excluded.
| if (maxTopUpWbtc !== undefined) watchdog.maxTopUpWbtc = maxTopUpWbtc; | |
| if (maxTopUpWbtc !== undefined) watchdog.maxTopUpWbtc = maxTopUpWbtc; | |
| const deadlineSecondsRaw = process.env['WATCHDOG_DEADLINE_SECONDS']; | |
| if (deadlineSecondsRaw) { | |
| const parsedDeadline = Number(deadlineSecondsRaw.trim()); | |
| if (Number.isFinite(parsedDeadline) && parsedDeadline > 0) { | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| (watchdog as any).deadlineSeconds = parsedDeadline; | |
| } | |
| } | |
| const rescueContractEnv = process.env['WATCHDOG_RESCUE_CONTRACT']; | |
| if (rescueContractEnv && rescueContractEnv.trim()) { | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| (watchdog as any).rescueContract = rescueContractEnv.trim(); | |
| } |
| const [currentHF, maxHF] = await Promise.all([ | ||
| this.previewResultingHF(provider, rescueContract, user, 0n), | ||
| this.previewResultingHF(provider, rescueContract, user, maxAmount), | ||
| ]); |
There was a problem hiding this comment.
The comment on line 352 states "HF is linear in amount" to justify the two-point interpolation. However, HF as a function of added collateral amount is not strictly linear — it depends on the weighted average liquidation threshold, which changes as new collateral is added (the new asset's LT may differ from the existing weighted LT). The linear approximation works reasonably well for small amounts relative to total collateral, but the comment should note this is an approximation, not an exact relationship.
| totalSuppliedUsd: 3_200, | ||
| totalBorrowedUsd: 1_600, |
There was a problem hiding this comment.
totalBorrowedUsd is set to 1_600 (with underscore separator = 1600) but borrowed.amount and borrowed.usdValue on lines 34 and 36 are set to 1600 (no underscore). These values match, but note that the HF for this loan would be (3200 * 0.75) / 1600 = 1.5, which is below the default triggerHF of 1.65. This is intentional for triggering the watchdog in tests, but worth confirming the test fixture produces the expected HF. The fixture is consistent.
| function supply(address, uint256 amount, address onBehalfOf, uint16) external { | ||
| AccountData storage data = accountData[onBehalfOf]; | ||
| data.totalCollateralBase += amount / 100; // test-only simplified conversion | ||
| data.healthFactor += 0.2e18; |
There was a problem hiding this comment.
The MockPool.supply always adds a fixed 0.2e18 to healthFactor regardless of the amount supplied. This means test_preview_increases_with_amount (line 233) passes only because previewResultingHF is a pure calculation that doesn't call supply, but the actual rescue execution path cannot meaningfully test that different amounts produce different HF outcomes. Consider making the mock's HF increase proportional to amount for more realistic coverage of the rescue path.
| data.healthFactor += 0.2e18; | |
| // Make health factor change proportional to the supplied amount for more realistic testing | |
| data.healthFactor += amount / 100; |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Approve type(uint256).max in _forceApprove to avoid per-rescue re-approval - Use toFixed(4) instead of toFixed(18) to avoid floating-point artifacts - Add env overrides for WATCHDOG_DEADLINE_SECONDS and WATCHDOG_RESCUE_CONTRACT - Fix misleading "HF is linear" comment to note it's an approximation - Make MockPool HF increase proportional to supplied amount Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ment The reviewer incorrectly claimed HF is not linear in collateral amount. In fact HF = Σ(collateral_i * LT_i) / totalDebt — the numerator is linear in amount and totalDebt is constant, so HF IS exactly linear. The MockPool `amount / 100` suggestion also broke tests: supplying 10_000_000 (0.1 WBTC) would add only 100_000 to an 18-decimal HF, effectively zero. Restored the fixed 0.2e18 increment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
AaveAtomicRescueV1, deploy script, and contract testsBreaking changes
minResultingHF,maxTopUpWbtc,deadlineSeconds,rescueContract)Validation
forgemissing)