feat: add fund recovery contracts#121
Open
shinchann221 wants to merge 7 commits intomasterfrom
Open
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e94774149e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Certora Run Started (Certora verification)
Certora Run Summary
|
…ck TopUp funds Adds RecoveryModule on Optimism (OApp sender, non-upgradable) + RecoveryDispatcher (OApp receiver, UUPS upgradable) on each destination chain, plus TopUpV2 with a dispatcher-gated executeRecovery. Owners sign a digest binding chainid, module, nonce, safe, token, amount, recipient, destEid and keccak256(lzOptions); the single recover() call verifies sigs and ships the LZ v2 message in the same tx. Includes Optimism deploy script + per-dest dispatcher and TopUp upgrade scripts (CREATE3, deterministic), an LZ peer-configuration script, and a verifier that asserts module/dispatcher immutables (endpoint, dataProvider, role registry) plus EIP-1967 impl slot, owner, SOURCE_EID, and optional beacon impl pointer. RecoveryDispatched event arg order matches RecoverySent.
0b62545 to
c88f288
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c88f288. Configure here.
shivam-ef
reviewed
Apr 29, 2026
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.

Fund Recovery Module
Summary
Self-service path for ERC20s stuck in wrong-chain TopUp contracts. User signs on their OP cash Safe, LayerZero v2 ships the request to one of 5 dest chains, dispatcher sweeps the stuck balance into a recipient address. Replaces the bespoke CX-driven recovery process and clears the ~700-case backlog. Source = Optimism (EID
30111); destinations = Ethereum, Arbitrum, Base, BNB, HyperEVM.Changes
AssetRecoveryModule.sol(Optimism, LZ v2 OAppSender): singlerecover(safe, token, recipient, safeSalt, destEid, lzOptions, signers, sigs)payable that:keccak256(lzOptions))msg.valueover the quoted feeRoleRegistryAssetRecoveryDispatcher.sol(per dest chain, LZ v2 OAppReceiver, UUPS): receives the LZ message and forwards toTopUpV2(payload.safe).executeRecovery(...).TopUpFactoryifpayload.safe.code.length == 0— covers users who only ever sent unsupported tokens to a chain, so the regular topup batch path never triggered a deploy. Salt is asserted againstfactory.getDeterministicAddress(salt) == payload.safefirst so a bogus salt can't litter the chain with stray TopUps.TopUpV2.sol(beacon impl on each dest chain): addsexecuteRecovery(token, recipient)gated to the local dispatcher. Sweeps fullbalanceOf(this). Reverts withOnlyUnsupportedTokensif the token has a configured bridge route (those must use the normal claim path) and withNoBalanceToRecoverif there's nothing to sweep — both keep the LZ packet retryable.RecoveryMessageLib.sol: encode/decode for(safe, token, recipient, salt). No amount field.Deploy + verify scripts (
scripts/recovery/): CREATE3 via Nick's factory so impls land at the same address on every chain. Per-chain runbook inscripts/recovery/README.md.VerifyRecoveryDeployment.s.solasserts post-deploy invariants (predicted addresses, owner = operating safe, endpoint immutable, source EID, role registry, TopUp factory).Peer wiring (
ConfigureLzPeers.s.sol): emits 14setPeercalldatas (7 OP module + 7 dispatchers) for the operating safe to 3CP-sign.Operations
AssetRecoveryModule(OP) and eachAssetRecoveryDispatchervia the operating safe0xA6cf33124cb342D1c604cAC87986B965F428AAC4.setPeer(eid, bytes32(0))on the OP module.Files Changed
src/modules/recovery/AssetRecoveryModule.sol(new)src/top-up/AssetRecoveryDispatcher.sol(new)src/top-up/TopUpV2.sol(new)src/libraries/RecoveryMessageLib.sol(new)src/interfaces/IAssetRecoveryModule.sol(new)scripts/recovery/DeployAssetRecoveryModule.s.sol(new)scripts/recovery/DeployAssetRecoveryDispatcher.s.sol(new)scripts/recovery/UpgradeTopUpImpl.s.sol(new)scripts/recovery/ConfigureLzPeers.s.sol(new)scripts/recovery/VerifyRecoveryDeployment.s.sol(new)scripts/recovery/RecoveryDeployConfig.sol(new)scripts/recovery/lz-config.json(new)scripts/recovery/README.md(new)test/integration/RecoveryE2E.t.sol(new)test/safe/modules/recovery/AssetRecoveryModule.t.sol(new)test/top-up/AssetRecoveryDispatcher.t.sol(new)test/top-up/TopUpV2.t.sol(new)test/top-up/TopUpV2_StorageLayout.t.sol(new)lib/LayerZero-v2,lib/devtools(submodules)remappings.txt,foundry.lock,.gitmodules(LZ deps)certora/confs/{CashModuleCore,DebtManagerCore,EtherFiSafe}.conf(point atSettlementDispatcherV2)Note
High Risk
Introduces new cross-chain messaging and sweeping logic (
AssetRecoveryModule/AssetRecoveryDispatcher) plus a beacon implementation upgrade path (TopUpV2), which is security- and funds-movement-critical and depends on correct peer/endpoint configuration across multiple chains.Overview
Adds a new LayerZero v2–based fund recovery path to sweep ERC20 balances stuck in wrong-chain
TopUpaddresses: an OptimismAssetRecoveryModule.recover()verifies Safe-owner signatures (nonce, salt, destEid, andlzOptionshash), dispatches the LZ message, and emitsRecoverySentwith overpayment refund support and pause/unpause viaRoleRegistry.On destination chains, introduces an upgradeable singleton
AssetRecoveryDispatcherthat validates source EID/peer, optionally lazy-deploys the user’s TopUp viaTopUpFactoryusing the payload salt, and forwards to a new beacon implementationTopUpV2.executeRecovery()which is dispatcher-gated and sweeps the full balance (reverting when empty or for supported tokens).Includes CREATE3-based deploy/verify/runbook tooling under
scripts/recovery/, adds LayerZero dependencies/remappings (new submodules +foundry.lock), updates dev deployment JSONs, pins the HyperEVM fork test to a fixed block, and adds unit + E2E tests plus mocks to cover the new recovery flow and storage-layout safety for the beacon upgrade.Reviewed by Cursor Bugbot for commit 7b5c276. Bugbot is set up for automated code reviews on this repo. Configure here.