Skip to content

Add Safe 4337 module migration helpers + fallback-handler reader#191

Open
Sednaoui wants to merge 11 commits into
devfrom
add-safe-module-migration-helpers
Open

Add Safe 4337 module migration helpers + fallback-handler reader#191
Sednaoui wants to merge 11 commits into
devfrom
add-safe-module-migration-helpers

Conversation

@Sednaoui
Copy link
Copy Markdown
Member

@Sednaoui Sednaoui commented May 29, 2026

What

Adds first-class support for migrating a deployed Safe between ERC-4337 module/EntryPoint versions, plus a way to read a Safe's current fallback handler. Surfaced while building a v0.7→v0.9 Safe migration example — the only existing helper covered v0.6→v0.7 (on SafeAccountV0_2_0), there was no reusable path to v0.9, and no reader for the fallback handler.

Changes

  • SafeAccount.createModuleMigrationMetaTransactions(nodeRpcUrl, oldModule, newModule, overrides) — generic builder returning [disableOld, enableNew, setFallbackHandler]. For Safe 4337 accounts the module is both the enabled module and the fallback handler, so that's the whole migration. Documented that no storage clearing is required: both Safe4337Module and Safe4337MultiChainSignatureModule are stateless.
  • SafeAccountV0_3_0.createMigrateToSafeMultiChainSigAccountV1MetaTransactions(nodeRpcUrl, overrides) — convenience wrapper for the common v0.7 → v0.9 path.
  • SafeAccountV0_2_0.createMigrateToSafeAccountV0_3_0MetaTransactions now delegates to the generic helper (removes the duplicated disable/enable/setFallback body; behavior preserved).
  • SafeAccount.getFallbackHandler(nodeRpcUrl) — reads the fallback handler (i.e. the active 4337 module) so callers can confirm which EntryPoint version an account is on after a migration.
  • JsonRpcNode.getStorageAt(address, slot, blockTag)eth_getStorageAt.
  • Export SAFE_FALLBACK_HANDLER_STORAGE_SLOT (keccak256("fallback_manager.handler.address")).

Verification

  • npm run build clean; 184/184 unit tests pass.
  • New API exercised against a real migrated Safe on Arbitrum Sepolia: getFallbackHandler() returns the v0.9 module, and the helper produces the correct 3-tx batch (disableModule / enableModule / setFallbackHandler).

Notes

The existing v0.6→v0.7 migration is now a thin wrapper over the generic helper — no public API removed or renamed; this is purely additive plus an internal de-duplication.

Summary by CodeRabbit

  • New Features

    • Read Safe fallback handler addresses directly from on-chain storage.
    • Build and orchestrate end-to-end Safe module migration flows for ERC-4337 modules, including v0.7→v0.9 upgrades and optional preflight validation.
    • Query arbitrary contract storage slots via RPC for direct on-chain state inspection.
  • Tests

    • Added deterministic unit tests and end-to-end integration tests covering module migrations, preflight checks, and storage reads.

Migrating a deployed Safe between EntryPoint versions means swapping its
4337 module and fallback handler. The only built-in helper covered v0.6 ->
v0.7; there was no reusable path to v0.9 and no way to read a Safe's current
fallback handler.

- SafeAccount.createModuleMigrationMetaTransactions(node, oldModule,
  newModule, overrides): generic [disableOld, enableNew, setFallbackHandler]
  builder. Both Safe4337Module and Safe4337MultiChainSignatureModule are
  stateless, so no storage clearing is needed.
- SafeAccountV0_3_0.createMigrateToSafeMultiChainSigAccountV1MetaTransactions:
  convenience wrapper for the v0.7 -> v0.9 path.
- SafeAccountV0_2_0.createMigrateToSafeAccountV0_3_0MetaTransactions now
  delegates to the generic helper (removes duplicated body).
- SafeAccount.getFallbackHandler(node): reads the fallback handler (the 4337
  module) so callers can confirm which EntryPoint version an account is on.
- JsonRpcNode.getStorageAt(address, slot, blockTag): eth_getStorageAt.
- Export SAFE_FALLBACK_HANDLER_STORAGE_SLOT constant.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds SAFE_FALLBACK_HANDLER_STORAGE_SLOT and JsonRpcNode.getStorageAt; implements SafeAccount.getFallbackHandler and createModuleMigrationMetaTransactions; updates V0_2_0 to delegate to the helper; adds V0_3_0 migration helper; re-exports the storage slot; and adds unit and e2e tests.

Changes

Safe Module Migration Infrastructure

Layer / File(s) Summary
Storage constant and RPC storage read
src/constants.ts, src/transport/JsonRpcNode.ts, test/transport/JsonRpcNode.test.js
Adds SAFE_FALLBACK_HANDLER_STORAGE_SLOT and JsonRpcNode.getStorageAt(address, slot, blockTag) to read raw storage words via JSON-RPC and tests RPC params, blockTag, and BAD_DATA handling.
SafeAccount fallback handler & migration core
src/account/Safe/SafeAccount.ts
Imports the storage slot constant; adds getFallbackHandler(nodeRpcUrl) to extract the handler address from storage; adds createModuleMigrationMetaTransactions(...) to build disable/enable/set-fallback MetaTransactions and isVersionAtLeast.
Version-specific migration helpers
src/account/Safe/SafeAccountV0_2_0.ts, src/account/Safe/SafeAccountV0_3_0.ts
SafeAccountV0_2_0 delegates migration MetaTransaction construction to the core helper (adds prevModuleAddress override). SafeAccountV0_3_0 adds createMigrateToSafeMultiChainSigAccountV1MetaTransactions to migrate v0.7→v0.9 using the core helper and SafeMultiChainSigAccountV1 defaults.
Public barrel export
src/abstractionkit.ts
Re-exports SAFE_FALLBACK_HANDLER_STORAGE_SLOT from the package barrel.
Tests — unit & e2e
test/safe/moduleMigration.test.js, test/transport/JsonRpcNode.test.js, test/integration/e2e/migrate-safe-v07-to-v09/migrate.test.js
Adds deterministic unit tests for migration helpers, preflight and reader behavior, plus an e2e suite validating a v0.7→v0.9 migration and post-migration UserOperation flow.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant SafeAccount
  participant JsonRpcNode
  Caller->>SafeAccount: createModuleMigrationMetaTransactions(nodeRpcUrl, oldModule, newModule, overrides)
  SafeAccount->>JsonRpcNode: getStorageAt(safeAddress, SAFE_FALLBACK_HANDLER_STORAGE_SLOT)
  JsonRpcNode-->>SafeAccount: fallback handler storage word
  SafeAccount->>SafeAccount: build disable-old-module MetaTx
  SafeAccount->>SafeAccount: build enable-new-module MetaTx
  SafeAccount->>SafeAccount: build setFallbackHandler(newModule) MetaTx
  SafeAccount-->>Caller: [disableTx, enableTx, setFallbackTx]
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • sherifahmed990
  • andrewwahid

Poem

🐰 I nibble bytes in storage deep,
Fetching handlers Safes quietly keep.
Three small transactions, old to new they steer,
Modules swap places, the path is clear.
— a rabbit, twitching whiskers

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The description covers the primary changes (module migration helpers, fallback handler reader, storage access), includes verification details and testing results, but the template structure (Summary, Test, Risk/Compatibility sections) is not followed. Consider reformatting the description to match the template structure with explicit Summary, Test, and Risk/Compatibility sections for clarity and consistency.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: adding Safe 4337 module migration helpers and a fallback-handler reader, which are the core additions in this PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/account/Safe/SafeAccountV0_2_0.ts`:
- Around line 382-390: The prevModuleAddress argument passed into
createModuleMigrationMetaTransactions is incorrectly wired to
overrides.safeV06ModuleAddress (the module being disabled) instead of the
linked-list predecessor; update the call so prevModuleAddress is set to the
predecessor value (e.g. overrides.prevModuleAddress or the explicit predecessor
field provided by overrides) rather than overrides.safeV06ModuleAddress,
ensuring createModuleMigrationMetaTransactions and any downstream disableModule
call receive the actual previous-module address.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 233c1326-568f-4648-a4f7-8070eade1b52

📥 Commits

Reviewing files that changed from the base of the PR and between f479c6f and 17c51b8.

📒 Files selected for processing (6)
  • src/abstractionkit.ts
  • src/account/Safe/SafeAccount.ts
  • src/account/Safe/SafeAccountV0_2_0.ts
  • src/account/Safe/SafeAccountV0_3_0.ts
  • src/constants.ts
  • src/transport/JsonRpcNode.ts

Comment thread src/account/Safe/SafeAccountV0_2_0.ts
Sednaoui added 2 commits May 29, 2026 12:30
createMigrateToSafeAccountV0_3_0MetaTransactions passed
overrides.safeV06ModuleAddress (the module being disabled) as
prevModuleAddress (the linked-list predecessor). When a caller set
safeV06ModuleAddress explicitly, this produced disableModule(prev=module,
module) — claiming the module precedes itself. Default callers were
unaffected (undefined -> on-chain predecessor lookup).

Add a proper prevModuleAddress override and stop mis-wiring it; the
predecessor is still looked up on-chain when not provided.
…geAt

Unit (offline, deterministic):
- test/safe/moduleMigration.test.js: createModuleMigrationMetaTransactions
  (selectors/targets/order, explicit predecessor, and on-chain predecessor
  lookup via mock transport), createMigrateToSafeMultiChainSigAccountV1-
  MetaTransactions (defaults + overrides), getFallbackHandler, and a
  regression for the v0.6->v0.7 prevModuleAddress wiring (prev != module).
- test/transport/JsonRpcNode.test.js: getStorageAt params, block tag, and
  BAD_DATA on non-string.

Integration (e2e, cross-EntryPoint):
- test/integration/e2e/migrate-safe-v07-to-v09/migrate.test.js: deploy a
  SafeAccountV0_3_0 on EP v0.7, migrate to the v0.9 multi-chain module,
  assert the on-chain module/fallback-handler swap, then execute a userop
  through SafeMultiChainSigAccountV1 on EP v0.9.
Copy link
Copy Markdown
Member

@sherifahmed990 sherifahmed990 left a comment

Choose a reason for hiding this comment

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

LGTM

Comment thread src/account/Safe/SafeAccount.ts Outdated
* @param overrides - overrides for finding the previous module
* @returns a promise of [disableOld, enableNew, setFallbackHandler] MetaTransactions
*/
public async createModuleMigrationMetaTransactions(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I suggest making this function protected

modulesStart?: string;
modulesPageSize?: bigint;
} = {},
): Promise<MetaTransaction[]> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I suggest verifying the safe version is compatible with the target Class

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I will add a check for minimum version

sherifahmed990 and others added 5 commits June 1, 2026 02:21
Before building a module-migration batch, verify on-chain that the account
is actually a Safe running the old 4337 module — the module is enabled AND
is the current fallback handler — and that its Safe version meets the module
minimum (>= 1.4.1). This turns a cryptic on-chain AA23/AA24 into a clear
up-front error. Opt out with { skipPreflight: true }.

- SafeAccount.getSafeVersion(node): reads VERSION().
- SafeAccount.createModuleMigrationMetaTransactions: runs the preflight unless
  skipped; threaded through the v0.6->v0.7 and v0.7->v0.9 wrappers.
- Note: an equality check against the target class's singleton would be wrong
  (it would reject valid 1.5.0 Safes); the modules only need >= 1.4.1, so the
  check is a minimum, not a match.

Tests cover the preflight pass/fail cases, skipPreflight bypass, getSafeVersion,
and getFallbackHandler.
dev's ethers-minimization dropped the AbiCoder import from SafeAccount.ts;
getSafeVersion now decodes VERSION() via the local decodeAbiParameters helper
(matching isModuleEnabled/getModules), fixing a runtime ReferenceError that
the bundler build did not catch.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/account/Safe/SafeAccount.ts`:
- Around line 3158-3174: The preflight checks can leak raw RPC/ABI errors from
isModuleEnabled(node, oldModuleAddress) and getSafeVersion(node) or pass through
empty/invalid VERSION() results; catch any thrown errors or invalid/empty
versions around those calls (references: isModuleEnabled, getSafeVersion,
SafeAccount.isVersionAtLeast, MIN_SAFE_4337_VERSION) and rethrow a normalized
AbstractionKitError("BAD_DATA", ...) that includes a clear message referencing
the Safe (this.accountAddress), the module (oldModuleAddress) and the
skipPreflight hint; ensure you wrap both the module-enabled check and the
version fetch/compare in try/catch and treat empty/invalid version responses as
failing the preflight so the same BAD_DATA error is emitted.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e324101a-4a0f-4d1a-8a0c-7c4ddaf487da

📥 Commits

Reviewing files that changed from the base of the PR and between 8c9b07c and 31b450d.

📒 Files selected for processing (4)
  • src/account/Safe/SafeAccount.ts
  • src/account/Safe/SafeAccountV0_2_0.ts
  • src/account/Safe/SafeAccountV0_3_0.ts
  • test/safe/moduleMigration.test.js
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/account/Safe/SafeAccountV0_3_0.ts
  • src/account/Safe/SafeAccountV0_2_0.ts
  • test/safe/moduleMigration.test.js

Comment thread src/account/Safe/SafeAccount.ts Outdated
Sednaoui added 3 commits June 1, 2026 16:11
The migration preflight already wrapped getFallbackHandler, but a raw RPC/ABI
error from isModuleEnabled or getSafeVersion (e.g. a reverting VERSION() on a
non-Safe), or an empty/invalid version string, could leak through unnormalized.
Wrap both calls in try/catch and treat empty/invalid versions as a failed
preflight, rethrowing a clear AbstractionKitError("BAD_DATA", ...) that names
the Safe, the module, and the skipPreflight hint.
It's the shared implementation behind the version-specific migration helpers,
which pin the correct module addresses. Marking it protected steers developers
to those wrappers instead of supplying raw old/new module addresses directly
(easy to get wrong). Subclass wrappers still call it via `this`. Tests now
exercise the shape and predecessor lookup through the public wrapper.
…7 fix

Document under [UNRELEASED]: the Safe v0.7->v0.9 migration helper with opt-out
preflight, the getFallbackHandler/getSafeVersion/getStorageAt readers and the
SAFE_FALLBACK_HANDLER_STORAGE_SLOT export, and the prevModuleAddress fix in the
v0.6->v0.7 migration helper.
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