Skip to content

[Provers L01] Change Ownable to Ownable2Step in StateProverPointer#88

Merged
luiz-lvj merged 5 commits intomainfrom
fix/provers-L01
Feb 25, 2026
Merged

[Provers L01] Change Ownable to Ownable2Step in StateProverPointer#88
luiz-lvj merged 5 commits intomainfrom
fix/provers-L01

Conversation

@luiz-lvj
Copy link
Collaborator

@luiz-lvj luiz-lvj commented Feb 23, 2026

StateProverPointer is governed by a single owner that can update the active StateProver via setImplementationAddress. Since this address selection directly determines which implementation is trusted for proof verification, an accidental or incorrect ownership transfer can permanently remove the ability to patch or upgrade proof verification on a given chain.

The contract currently inherits OpenZeppelin Ownable, where transferOwnership(newOwner) immediately assigns ownership to newOwner without requiring any action from the recipient. In practice, governance transitions for public infrastructure often involve multisigs, DAOs, or staged operational handoffs across many chains; a single-step transfer increases the chance that an incorrect address (wrong chain deployment, misconfigured multisig, incorrect governance executor, etc.) receives ownership.

If ownership is transferred to an address that cannot operate as owner (or to an unintended address), the pointer can become effectively bricked:

The implementation can no longer be updated to fix bugs or respond to vulnerabilities.
Downstream integrators that depend on the pointer’s stability may be forced into coordinated migrations, which is operationally expensive and error-prone for widely-used infrastructure.

This PR updates the StateProverPointer to use Ownable2Step instead of Ownable

Summary by CodeRabbit

  • New Features

    • Enhanced ownership management with two-step transfer for improved security.
  • Tests

    • Added tests to verify two-step ownership transfer flows.
  • Chores

    • Updated verification snapshot counts used for validations.

@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e057638 and ae8bdab.

📒 Files selected for processing (2)
  • snapshots/verifyBroadcastMessage.json
  • src/contracts/StateProverPointer.sol
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/contracts/StateProverPointer.sol

📝 Walkthrough

Walkthrough

Updated numeric counts in a snapshot JSON and changed StateProverPointer to use OpenZeppelin's Ownable2Step; added a test covering two-step ownership transfer.

Changes

Cohort / File(s) Summary
Snapshot Data Update
snapshots/verifyBroadcastMessage.json
Adjusted several numeric string counts for various broadcast message directions (EthereumToOptimism, EthereumToTaikoL2, LineaL2ToEthereum, ScrollL2ToEthereum, TaikoL2ToEthereum, ZkSyncL2ToEthereum).
Contract Ownership
src/contracts/StateProverPointer.sol
Replaced Ownable inheritance with Ownable2Step and added the corresponding import. No other public API changes reported.
Ownership Transfer Tests
test/StateProverPointer.t.sol
Added test_transferOwnership() to exercise two-step ownership transfer, acceptance, transfer-back, and pending-owner cancellation flows.

Sequence Diagram(s)

(No sequence diagrams generated.)

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Suggested reviewers

  • pepebndc

Poem

🐰
I hopped through JSON, numbers bright,
Switched the key to two-step light,
Owners pass the baton slow,
Tests confirm the gentle flow,
A tiny hop, a safer night.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: replacing Ownable with Ownable2Step in the StateProverPointer contract, which aligns with the primary objective and file changes.
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
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/provers-L01

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.

@luiz-lvj luiz-lvj requested review from frangio and pepebndc February 23, 2026 13:08
Copy link

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

Caution

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

⚠️ Outside diff range comments (2)
src/contracts/StateProverPointer.sol (2)

17-23: ⚠️ Potential issue | 🟠 Major

renounceOwnership() is not overridden — single-step permanent ownership loss is still possible.

Ownable2Step inherits renounceOwnership() from Ownable unchanged. Any call to renounceOwnership() immediately and irrevocably sets the owner to address(0), skipping the two-step guard entirely. If the owner is a multisig or governance contract that accidentally includes a renounceOwnership() call in a batch, the pointer is permanently bricked — no further setImplementationAddress is ever possible, forcing the expensive downstream migration the PR is explicitly trying to avoid.

There is still no two-step verification for renouncing ownership, i.e. transferring ownership to the zero address. If there is no need to renounce ownership, it is safer to override renounceOwnership() to revert when called.

🔒 Proposed fix — disable renounceOwnership
     constructor(address _initialOwner) Ownable(_initialOwner) {}

+    /// `@notice` Renouncing ownership is disabled; use transferOwnership/acceptOwnership instead.
+    function renounceOwnership() public override onlyOwner {
+        revert("renounceOwnership disabled");
+    }
+
     /// `@notice` Returns the address of the current StateProver implementation
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/contracts/StateProverPointer.sol` around lines 17 - 23, The contract
StateProverPointer currently inherits renounceOwnership() from Ownable via
Ownable2Step which allows immediate permanent ownership loss; override
renounceOwnership() in StateProverPointer to revert instead (e.g.,
revert("renounceOwnership disabled")) so the two‑step transfer semantics cannot
be bypassed, and ensure this prevents accidental bricking that would block calls
like setImplementationAddress; reference renounceOwnership(),
StateProverPointer, Ownable2Step and setImplementationAddress when locating
where to add the override.

56-62: ⚠️ Potential issue | 🟡 Minor

Unguarded high-level version() call on the current implementation can brick upgrades.

Line 58 calls IStateProver(currentImplementationAddress).version() directly via a high-level call that will revert and propagate if the current implementation's version() reverts (e.g., malicious or broken proxy). By contrast, the new-address path at lines 48–52 correctly uses a protected staticcall. If the current implementation ever becomes non-callable, setImplementationAddress becomes permanently blocked.

🛡️ Proposed fix — use try/catch for the old version lookup
-        uint256 oldVersion = IStateProver(currentImplementationAddress).version();
-        if (newVersion <= oldVersion) {
-            revert NonIncreasingVersion(newVersion, oldVersion);
+        try IStateProver(currentImplementationAddress).version() returns (uint256 oldVersion) {
+            if (newVersion <= oldVersion) {
+                revert NonIncreasingVersion(newVersion, oldVersion);
+            }
+        } catch {
+            revert InvalidImplementationAddress();
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/contracts/StateProverPointer.sol` around lines 56 - 62, The direct
high-level call IStateProver(currentImplementationAddress).version() inside the
upgrade path can revert and permanently block setImplementationAddress; replace
it with a guarded try/catch around the external call (or perform a low-level
staticcall) so a failing/ malicious old implementation does not prevent
upgrades. Concretely, in the function that reads implementationAddress() (e.g.,
setImplementationAddress), wrap the version lookup in: try
IStateProver(currentImplementationAddress).version() returns (uint256
oldVersion) { if (newVersion <= oldVersion) revert
NonIncreasingVersion(newVersion, oldVersion); } catch { /* ignore failure and
allow upgrade to proceed */ } so failures in the old contract don’t propagate
and block upgrades.
🧹 Nitpick comments (2)
src/contracts/StateProverPointer.sol (2)

43-66: Consider emitting an event when the implementation is successfully updated.

setImplementationAddress writes critical state (new implementation address + code hash) but emits no event. Without an event, off-chain monitors, indexers, and cross-chain verifiers have no log-based signal when the pointer changes — they must resort to polling storage.

📡 Suggested event
+    event ImplementationAddressUpdated(
+        address indexed previousImplementation,
+        address indexed newImplementation,
+        uint256 newVersion
+    );

     function setImplementationAddress(address _newImplementationAddress) external onlyOwner {
         ...
         _implementationAddress = _newImplementationAddress;
         _setCodeHash(_newImplementationAddress.codehash);
+        emit ImplementationAddressUpdated(currentImplementationAddress, _newImplementationAddress, newVersion);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/contracts/StateProverPointer.sol` around lines 43 - 66, Add an event
declaration (e.g., ImplementationUpdated(address indexed implementation, uint256
version, bytes32 codeHash)) to the contract and emit it at the end of
setImplementationAddress after updating _implementationAddress and _setCodeHash;
include the new implementation address, the decoded newVersion, and the new code
hash (_newImplementationAddress.codehash) so off-chain consumers can detect
pointer changes via logs; ensure the event name and field names match your
project's naming conventions and are declared at contract scope.

2-2: Pragma pinned to 0.8.30; latest release is 0.8.34.

Solidity 0.8.34 was released on February 18, 2026 as a bugfix release that patches an important bug of high severity affecting clearing of storage and transient storage variables in the IR pipeline. The bug affects compiler versions 0.8.28 through 0.8.33 and only manifests when compiling with the IR pipeline; since --via-ir is not enabled by default, projects that have not explicitly opted into it are not affected. This contract does not use transient storage with delete, so it is not affected by the bug. Nonetheless, consider bumping the pragma to 0.8.34 (or a minimum range like ^0.8.30) to stay on a supported, patched compiler.

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

In `@src/contracts/StateProverPointer.sol` at line 2, The pragma is pinned to
0.8.30; update the solidity version specifier in StateProverPointer (the pragma
line) to a patched compiler release or a safe range—either bump it to 0.8.34 or
use a caret range like ^0.8.30—to ensure you compile with a supported bugfixed
compiler; modify the pragma statement accordingly in StateProverPointer.sol.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/contracts/StateProverPointer.sol`:
- Around line 17-23: The contract StateProverPointer currently inherits
renounceOwnership() from Ownable via Ownable2Step which allows immediate
permanent ownership loss; override renounceOwnership() in StateProverPointer to
revert instead (e.g., revert("renounceOwnership disabled")) so the two‑step
transfer semantics cannot be bypassed, and ensure this prevents accidental
bricking that would block calls like setImplementationAddress; reference
renounceOwnership(), StateProverPointer, Ownable2Step and
setImplementationAddress when locating where to add the override.
- Around line 56-62: The direct high-level call
IStateProver(currentImplementationAddress).version() inside the upgrade path can
revert and permanently block setImplementationAddress; replace it with a guarded
try/catch around the external call (or perform a low-level staticcall) so a
failing/ malicious old implementation does not prevent upgrades. Concretely, in
the function that reads implementationAddress() (e.g.,
setImplementationAddress), wrap the version lookup in: try
IStateProver(currentImplementationAddress).version() returns (uint256
oldVersion) { if (newVersion <= oldVersion) revert
NonIncreasingVersion(newVersion, oldVersion); } catch { /* ignore failure and
allow upgrade to proceed */ } so failures in the old contract don’t propagate
and block upgrades.

---

Nitpick comments:
In `@src/contracts/StateProverPointer.sol`:
- Around line 43-66: Add an event declaration (e.g.,
ImplementationUpdated(address indexed implementation, uint256 version, bytes32
codeHash)) to the contract and emit it at the end of setImplementationAddress
after updating _implementationAddress and _setCodeHash; include the new
implementation address, the decoded newVersion, and the new code hash
(_newImplementationAddress.codehash) so off-chain consumers can detect pointer
changes via logs; ensure the event name and field names match your project's
naming conventions and are declared at contract scope.
- Line 2: The pragma is pinned to 0.8.30; update the solidity version specifier
in StateProverPointer (the pragma line) to a patched compiler release or a safe
range—either bump it to 0.8.34 or use a caret range like ^0.8.30—to ensure you
compile with a supported bugfixed compiler; modify the pragma statement
accordingly in StateProverPointer.sol.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ffa452d and e057638.

📒 Files selected for processing (3)
  • snapshots/verifyBroadcastMessage.json
  • src/contracts/StateProverPointer.sol
  • test/StateProverPointer.t.sol


import {StorageSlot} from "@openzeppelin/contracts/utils/StorageSlot.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this import still needed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

in order to use Ownable2Steps we need to import both Ownable2Steps and Ownable, because the constructor takes the Ownable:

constructor(address _initialOwner) Ownable(_initialOwner) {}

@luiz-lvj luiz-lvj requested a review from pepebndc February 24, 2026 17:25
@luiz-lvj luiz-lvj requested a review from frangio February 24, 2026 19:33
@luiz-lvj luiz-lvj merged commit 06a75c8 into main Feb 25, 2026
3 checks passed
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.

3 participants