Skip to content

fix(account): revert onInstall when module already installed with different config#229

Open
rahul-0407 wants to merge 2 commits into
OpenZeppelin:masterfrom
rahul-0407:fix/erc7579-signature-oninstall-noop
Open

fix(account): revert onInstall when module already installed with different config#229
rahul-0407 wants to merge 2 commits into
OpenZeppelin:masterfrom
rahul-0407:fix/erc7579-signature-oninstall-noop

Conversation

@rahul-0407

@rahul-0407 rahul-0407 commented Apr 17, 2026

Copy link
Copy Markdown

Summary

Fixes #192

The onInstall function in both ERC7579Signature and ERC7579Multisig
previously did a silent no-op when called on an already-installed module.
This is dangerous because the caller may expect the new config to be active,
leading to silent misconfigurations.

Changes

ERC7579Signature

  • Added ERC7579SignatureAlreadyInstalled error
  • onInstall now reverts if a different signer is already set
  • Idempotent re-installation with the same signer data is still allowed
  • onInstall still validates signer length (>= 20 bytes)

ERC7579Multisig

  • Added ERC7579MultisigAlreadyInstalled error
  • onInstall now reverts if signers are already configured

Tests

  • ERC7579SignatureValidator.test.js: replaced no-op test with 3 targeted tests
  • ERC7579Multisig.test.js: updated expectation from no-op to revert

Test Results

  • ERC7579Signature: 20/20 ✅
  • ERC7579Multisig: 13/13 ✅

Summary by CodeRabbit

  • Bug Fixes

    • Module installation now reverts when attempting to reinstall on an already-configured account
    • Added stricter validation for signer configuration with explicit error handling for invalid operations
  • Tests

    • Updated test suite to verify new installation protection behavior and signer validation rules

…ferent config

- ERC7579Signature: revert with ERC7579SignatureAlreadyInstalled when a different
  signer is already set; allow idempotent reinstall with same data
- ERC7579Multisig: revert with ERC7579MultisigAlreadyInstalled when signers
  already configured
- Update tests for both modules to expect revert instead of no-op

Fixes OpenZeppelin#192
@rahul-0407 rahul-0407 requested a review from a team as a code owner April 17, 2026 19:22
@coderabbitai

coderabbitai Bot commented Apr 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5b197a55-dd29-4bd6-96b6-7ed9c3d2573d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Updated module installation behavior across two ERC7579 modules to revert on repeated installation attempts instead of silently succeeding. Added custom errors and validation logic to enforce single-installation semantics, with an exception in the signature module allowing idempotent calls with identical signer data.

Changes

Cohort / File(s) Summary
Contract Implementation Updates
contracts/account/modules/ERC7579Multisig.sol, contracts/account/modules/ERC7579Signature.sol
Added custom error declarations and updated onInstall() logic to enforce installation guard checks. Multisig module now unconditionally reverts if signers already exist. Signature module reverts only if a different signer is already present, allowing idempotent calls with matching data and enforcing minimum 20-byte signer length validation.
Test Suite Updates
test/account/modules/ERC7579Multisig.test.js, test/account/modules/ERC7579SignatureValidator.test.js
Updated test cases to verify new revert behavior on repeated installation. Multisig tests now expect revert with ERC7579MultisigAlreadyInstalled. Signature validator tests split previous no-op coverage into three distinct cases: successful first install, idempotent reinstalls with same data, and revert on conflicting signer installation.

Poem

🐰 Hop, hop, the modules now refuse
To install twice without a fuse,
Once is enough, we make it clear—
Guard against accidental cheer!
Safe and sound, our state so tight,
One install rule keeps code right. 🛡️

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: reverting onInstall when modules are already installed with different configurations.
Linked Issues check ✅ Passed The PR implements the proposed behavior from issue #192: reverting on conflicting installs while allowing idempotent re-installs with identical config.
Out of Scope Changes check ✅ Passed All changes directly address issue #192 by modifying onInstall behavior in ERC7579Signature and ERC7579Multisig modules and updating corresponding tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
contracts/account/modules/ERC7579Signature.sol (1)

38-52: ⚠️ Potential issue | 🟡 Minor

Stale docstring and unnecessary re-_setSigner on idempotent install.

  1. Lines 41-42 still say "Future installations will behave as a no-op", but the new behavior is: revert on mismatched data, otherwise (re)set. Please update the NOTE to reflect the revert + idempotent-same-data semantics (mirroring the NOTE you added in ERC7579Multisig.sol).

  2. When currentSigner.length != 0 and keccak256(currentSigner) == keccak256(data), you still call _setSigner, which re-emits ERC7579SignatureSignerSet with identical data. That is noisy for indexers observing install events and performs a redundant SSTORE. Consider short-circuiting when the signer is already equal.

Proposed change
     /**
      * `@dev` See {IERC7579Module-onInstall}.
      *
-     * NOTE: An account can only call onInstall once. If called directly by the account,
-     * the signer will be set to the provided data. Future installations will behave as a no-op.
+     * NOTE: On first install, the signer is set to the provided `data`. Subsequent calls are
+     *       idempotent only when `data` matches the currently installed signer; otherwise they
+     *       revert with {ERC7579SignatureAlreadyInstalled}.
      */
     function onInstall(bytes calldata data) public virtual {
         bytes memory currentSigner = signer(msg.sender);
-        require(
-            currentSigner.length == 0 || keccak256(currentSigner) == keccak256(data),
-            ERC7579SignatureAlreadyInstalled()
-        );
-        require(data.length >= 20, ERC7579SignatureInvalidSignerLength());
-        _setSigner(msg.sender, data);
+        if (currentSigner.length == 0) {
+            require(data.length >= 20, ERC7579SignatureInvalidSignerLength());
+            _setSigner(msg.sender, data);
+        } else {
+            require(keccak256(currentSigner) == keccak256(data), ERC7579SignatureAlreadyInstalled());
+            // idempotent: no-op for identical data, no redundant event/SSTORE
+        }
     }

Also consider changing line 30's @notice to @dev for consistency with the surrounding error docs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/account/modules/ERC7579Signature.sol` around lines 38 - 52, Update
the onInstall doc comment NOTE to match the actual semantics (revert on
mismatched data and idempotently accept identical data, mirroring
ERC7579Multisig.sol) and change the earlier `@notice` tag to `@dev` for consistency;
then modify the onInstall function so that if signer(msg.sender) exists and
keccak256(currentSigner) == keccak256(data) you short-circuit and return without
calling _setSigner to avoid redundant SSTORE and duplicate
ERC7579SignatureSignerSet events — otherwise keep the existing require checks
and call _setSigner as before.
contracts/account/modules/ERC7579Multisig.sol (1)

71-78: ⚠️ Potential issue | 🔴 Critical

ERC7579MultisigWeighted.onInstall breaks with the new revert-on-reinstall semantics.

The base class change at commit 1489673 switched from conditional no-op (if (getSignerCount == 0)) to an unconditional revert (require(getSignerCount == 0)). The weighted subclass still calls super.onInstall(initData) directly after capturing the installed flag, causing it to revert whenever the account already has signers. This makes the !installed guard and weight-setting branch unreachable on reinstall attempts.

The test at lines 112–125 reflects the old behavior (expecting a no-op on second install) but does not expect or handle the new revert. Update ERC7579MultisigWeighted.onInstall to return early when installed, or document this as a breaking change and update tests accordingly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/account/modules/ERC7579Multisig.sol` around lines 71 - 78,
ERC7579MultisigWeighted.onInstall still calls super.onInstall(initData) after
checking an `installed` flag, but the base ERC7579Multisig.onInstall now
unconditionally reverts when signers exist; update
ERC7579MultisigWeighted.onInstall to check installation state via
getSignerCount(msg.sender) (or reuse the existing `installed` local) and return
early if already installed before calling super.onInstall, or alternatively call
super.onInstall only when not installed and perform weight-setting only in that
non-installed branch so the reinstall path no longer triggers the base-class
require.
🧹 Nitpick comments (2)
test/account/modules/ERC7579Multisig.test.js (1)

98-103: LGTM — good coverage of the new revert semantics.

The assertion correctly exercises the new branch by passing different signer/threshold bytes after a successful install.

Minor nit (pre-existing): installData at line 40 encodes threshold as uint256 while the contract decodes (bytes[], uint64). This works today because ABI tail values for uint64/uint256 are both 32 bytes, but it is technically a type mismatch — consider aligning to uint64 in a follow-up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/account/modules/ERC7579Multisig.test.js` around lines 98 - 103, The test
encodes installData using uint256 while the contract decodes (bytes[], uint64),
so update the ABI encoding used when constructing installData (the value passed
into this.mockFromAccount.onInstall and referenced in the test) to use 'uint64'
instead of 'uint256' (or otherwise cast the threshold to a 64-bit value) so the
encoded types match the contract decoder; adjust the call site that builds
installData and any helper that generates that encoded payload to use
['bytes[]','uint64'] (or an explicit 64-bit value) to eliminate the type
mismatch.
test/account/modules/ERC7579SignatureValidator.test.js (1)

73-99: LGTM — the three new cases cleanly cover first-install, idempotent re-install, and mismatched re-install.

One optional addition: add a case that calls onInstall with the same signer twice and asserts that the second call does not emit ERC7579SignatureSignerSet (or alternatively, does emit — whichever semantics you land on after the _setSigner short-circuit suggestion on ERC7579Signature.sol). That locks the event-emission contract for re-installs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/account/modules/ERC7579SignatureValidator.test.js` around lines 73 - 99,
Add a test that calls this.mockFromAccount.onInstall(signerData) twice and
asserts the second call does not (or does, depending on chosen semantics) emit
the ERC7579SignatureSignerSet event: use the existing signerData,
mockFromAccount and mockAccount identifiers, call onInstall once to set the
signer then call it a second time and assert event emission behavior with the
ERC7579SignatureSignerSet event name (or invert the assertion if you decided to
keep emission on idempotent installs); ensure the test checks signer remains
unchanged via this.mock.signer(this.mockAccount.address) as in the other cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@contracts/account/modules/ERC7579Signature.sol`:
- Around line 46-50: The current require ordering can misreport errors: validate
input length first by checking data.length >= 20 and revert with
ERC7579SignatureInvalidSignerLength() before enforcing the install/equality
guard; then perform the equality/install check using currentSigner and
keccak256(data) and revert with ERC7579SignatureAlreadyInstalled() if needed
(swap the two require statements involving data, currentSigner,
ERC7579SignatureAlreadyInstalled, and ERC7579SignatureInvalidSignerLength).

---

Outside diff comments:
In `@contracts/account/modules/ERC7579Multisig.sol`:
- Around line 71-78: ERC7579MultisigWeighted.onInstall still calls
super.onInstall(initData) after checking an `installed` flag, but the base
ERC7579Multisig.onInstall now unconditionally reverts when signers exist; update
ERC7579MultisigWeighted.onInstall to check installation state via
getSignerCount(msg.sender) (or reuse the existing `installed` local) and return
early if already installed before calling super.onInstall, or alternatively call
super.onInstall only when not installed and perform weight-setting only in that
non-installed branch so the reinstall path no longer triggers the base-class
require.

In `@contracts/account/modules/ERC7579Signature.sol`:
- Around line 38-52: Update the onInstall doc comment NOTE to match the actual
semantics (revert on mismatched data and idempotently accept identical data,
mirroring ERC7579Multisig.sol) and change the earlier `@notice` tag to `@dev` for
consistency; then modify the onInstall function so that if signer(msg.sender)
exists and keccak256(currentSigner) == keccak256(data) you short-circuit and
return without calling _setSigner to avoid redundant SSTORE and duplicate
ERC7579SignatureSignerSet events — otherwise keep the existing require checks
and call _setSigner as before.

---

Nitpick comments:
In `@test/account/modules/ERC7579Multisig.test.js`:
- Around line 98-103: The test encodes installData using uint256 while the
contract decodes (bytes[], uint64), so update the ABI encoding used when
constructing installData (the value passed into this.mockFromAccount.onInstall
and referenced in the test) to use 'uint64' instead of 'uint256' (or otherwise
cast the threshold to a 64-bit value) so the encoded types match the contract
decoder; adjust the call site that builds installData and any helper that
generates that encoded payload to use ['bytes[]','uint64'] (or an explicit
64-bit value) to eliminate the type mismatch.

In `@test/account/modules/ERC7579SignatureValidator.test.js`:
- Around line 73-99: Add a test that calls
this.mockFromAccount.onInstall(signerData) twice and asserts the second call
does not (or does, depending on chosen semantics) emit the
ERC7579SignatureSignerSet event: use the existing signerData, mockFromAccount
and mockAccount identifiers, call onInstall once to set the signer then call it
a second time and assert event emission behavior with the
ERC7579SignatureSignerSet event name (or invert the assertion if you decided to
keep emission on idempotent installs); ensure the test checks signer remains
unchanged via this.mock.signer(this.mockAccount.address) as in the other cases.
🪄 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: 6f765e58-80fc-4be3-82ee-dc4d5898d138

📥 Commits

Reviewing files that changed from the base of the PR and between a12b30c and 1489673.

📒 Files selected for processing (4)
  • contracts/account/modules/ERC7579Multisig.sol
  • contracts/account/modules/ERC7579Signature.sol
  • test/account/modules/ERC7579Multisig.test.js
  • test/account/modules/ERC7579SignatureValidator.test.js

Comment thread contracts/account/modules/ERC7579Signature.sol Outdated
- ERC7579Signature: fix require ordering, update NatSpec, short-circuit on idempotent reinstall
- ERC7579MultisigWeighted: return early when already installed to avoid base class revert
- Change @notice to @dev on ERC7579SignatureAlreadyInstalled error
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.

Consider reverting module installation calls when state is not set (instead of no op)

1 participant