Skip to content

fix(recovery): bind a signed deadline into AssetRecoveryModule (I-04 parity with SafeAssetRecoveryModule)#165

Open
seongyun-ko wants to merge 1 commit into
masterfrom
fix/recovery-lz-deadline
Open

fix(recovery): bind a signed deadline into AssetRecoveryModule (I-04 parity with SafeAssetRecoveryModule)#165
seongyun-ko wants to merge 1 commit into
masterfrom
fix/recovery-lz-deadline

Conversation

@seongyun-ko

@seongyun-ko seongyun-ko commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

AssetRecoveryModule (the LayerZero recovery variant) did not bind a signed deadline, while its sibling SafeAssetRecoveryModule does (audit finding I-04). This PR ports that mitigation so the two recovery modules are consistent.

Why

Both recovery modules sweep the full token balance and do not sign an amount — the destination sweeps whatever balance is present at delivery. SafeAssetRecoveryModule binds a signed deadline (I-04) precisely so a relayer cannot stash a valid owner signature and later replay it against a future, larger deposit of the same token.

AssetRecoveryModule bound no deadline, so a valid recovery signature never expired: the per-safe nonce made it single-use, but it remained replayable in time until consumed. Since the recovery caller is untrusted (EtherFi submits the owner-authorized recovery), a signed authorization for a small stuck balance could be held and dispatched later against a much larger balance. The recipient is bound in the digest (so this is not fund redirection), but the authorization should be time-bounded like the sibling module.

Changes

  • recover() takes a deadline and reverts RecoveryExpired once block.timestamp > deadline
  • deadline is bound into the recovery digest (mirrors SafeAssetRecoveryModule)
  • IAssetRecoveryModule: add RecoveryExpired error + deadline param
  • update LZ recover callers in the unit + E2E suites
  • add test_recover_revertsIfExpired regression (parity with the safe variant)

Test plan

TEST_CHAIN=10 forge test --match-contract "AssetRecoveryModuleTest|SafeAssetRecoveryModuleTest|RecoveryE2E"
  • 29/29 pass (14 AssetRecoveryModule incl. new expiry regression, 14 SafeAssetRecoveryModule, 1 RecoveryE2E)

Note for reviewers

recover()'s signature changes (adds deadline). Any off-chain owner-signing tooling that builds the recovery digest must add deadline to the ABI-encoded digest, in the same position as the on-chain _verifyRecoverySignatures.


Note

Medium Risk
Changes fund-recovery authorization semantics and breaks the recover ABI/digest for integrators, but the guard is narrow and mirrors an existing sibling module pattern.

Overview
Adds a signed deadline to cross-chain AssetRecoveryModule.recover, matching SafeAssetRecoveryModule and closing audit I-04: destination recovery sweeps the full token balance with no signed amount, so a relayer could otherwise hold a valid owner signature until a larger balance arrived.

recover now takes uint256 deadline, reverts RecoveryExpired when block.timestamp > deadline, and includes deadline in the EIP-style recovery digest in _verifyRecoverySignatures. IAssetRecoveryModule gains the error and updated signature.

Unit and E2E tests pass deadline (default type(uint256).max); test_recover_revertsIfExpired covers post-deadline rejection. Breaking: off-chain digest/signing must ABI-encode deadline in the same slot as on-chain.

Reviewed by Cursor Bugbot for commit debb682. Bugbot is set up for automated code reviews on this repo. Configure here.

Port the audit I-04 mitigation from SafeAssetRecoveryModule to its
LayerZero sibling AssetRecoveryModule, which lacked it.

Both recovery modules perform a full-balance sweep and do not sign an
amount (the destination sweeps whatever balance is present at delivery).
SafeAssetRecoveryModule already binds a signed deadline so a relayer
cannot stash a valid owner signature and replay it against a future,
larger deposit of the same token. AssetRecoveryModule bound no deadline,
so a valid signature never expired: only the per-safe nonce made it
single-use, but it stayed replayable in time until consumed.

Changes:
- add a signed deadline param to recover(); revert RecoveryExpired past it
- bind deadline into the recovery digest (mirrors SafeAssetRecoveryModule)
- add RecoveryExpired error + deadline to IAssetRecoveryModule
- update the LZ recover callers (unit + E2E suites) and add a
  test_recover_revertsIfExpired regression

All 29 recovery/E2E tests pass (TEST_CHAIN=10).
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@certora-run certora-run Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verification Results

  • Group ID: 6a5992d8-9ef9-4f65-b1a2-6d33f233e88a
  • Commit: debb682
JobResultVERIFIED
EtherFiSafe.…14
CashModuleCo…2

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