Skip to content

feat: move watchdog to atomic on-chain rescue v1 (breaking)#17

Merged
felipecsl merged 11 commits intomasterfrom
feat/atomic-rescue-v1
Mar 9, 2026
Merged

feat: move watchdog to atomic on-chain rescue v1 (breaking)#17
felipecsl merged 11 commits intomasterfrom
feat/atomic-rescue-v1

Conversation

@felipecsl
Copy link
Member

@felipecsl felipecsl commented Mar 9, 2026

Summary

  • replace repay-based watchdog flow with atomic on-chain rescue submission
  • add Foundry Solidity package with AaveAtomicRescueV1, deploy script, and contract tests
  • migrate watchdog config/schema/UI to rescue-specific fields (breaking)
  • update docs and ops runbook for rescue v1

Breaking changes

  • watchdog config shape changed: removes repay-specific fields and adds rescue fields (minResultingHF, maxTopUpWbtc, deadlineSeconds, rescueContract)
  • server watchdog behavior now uses WBTC top-up rescue path only

Validation

  • yarn lint
  • yarn format
  • yarn typecheck
  • yarn test
  • yarn test:contracts (not executable in this environment: forge missing)

felipecsl and others added 7 commits March 8, 2026 22:13
…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>
@felipecsl felipecsl requested a review from Copilot March 9, 2026 15:45
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 AaveAtomicRescueV1 Solidity 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);
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

_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.

Suggested change
bool ok = IERC20(asset).approve(spender, amount);
bool ok = IERC20(asset).approve(spender, type(uint256).max);

Copilot uses AI. Check for mistakes.
}

private toWad(value: number): bigint {
return parseUnits(value.toFixed(18), 18);
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
if (minResultingHF !== undefined) watchdog.minResultingHF = minResultingHF;

const maxTopUpWbtc = parseEnvFloat('WATCHDOG_MAX_TOP_UP_WBTC');
if (maxTopUpWbtc !== undefined) watchdog.maxTopUpWbtc = maxTopUpWbtc;
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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();
}

Copilot uses AI. Check for mistakes.
Comment on lines +354 to +357
const [currentHF, maxHF] = await Promise.all([
this.previewResultingHF(provider, rescueContract, user, 0n),
this.previewResultingHF(provider, rescueContract, user, maxAmount),
]);
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +57 to +58
totalSuppliedUsd: 3_200,
totalBorrowedUsd: 1_600,
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
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;
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
data.healthFactor += 0.2e18;
// Make health factor change proportional to the supplied amount for more realistic testing
data.healthFactor += amount / 100;

Copilot uses AI. Check for mistakes.
felipecsl and others added 4 commits March 9, 2026 09:55
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>
@felipecsl felipecsl merged commit 219a507 into master Mar 9, 2026
1 check passed
@felipecsl felipecsl deleted the feat/atomic-rescue-v1 branch March 9, 2026 17:32
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