Skip to content

feat: add SetBatcherAndSigner template#1393

Merged
Wazabie merged 7 commits intomainfrom
jd/template-system-config-batcher-signer
Apr 21, 2026
Merged

feat: add SetBatcherAndSigner template#1393
Wazabie merged 7 commits intomainfrom
jd/template-system-config-batcher-signer

Conversation

@donoso-eth
Copy link
Copy Markdown
Contributor

@donoso-eth donoso-eth commented Apr 10, 2026

⚠️ Reviewers: This PR is part of the Superchain-ops Ownership Tracker workflow. Let's follow the steps

Review progress:

  • Solutions author creates PR and self-reviews with Claude skill (superchain-ops-template-review)
  • Solutions peer reviewer reviews PR with Claude skill (superchain-ops-template-review) — @Wazabie
  • Security review

Summary

Adds a reusable SetBatcherAndSigner template for atomically updating
batcherHash and unsafeBlockSigner on SystemConfig in a single batched
Multicall3 tx from FoundationUpgradeSafe. Mirrors the SystemConfigGasParams
pattern — same safe, same target, same storage-write surface.

Files

  • src/template/SetBatcherAndSigner.sol — template with inline ISystemConfig
  • test/tasks/example/sep/035-set-batcher-and-signer/ — example task for OP Sepolia
  • test/tasks/Regression.t.sol — pinned-calldata regression test

Test plan

  • forge build --deny-warnings / forge fmt --check / check-struct-order.sh clean
  • forge test --match-test SetBatcherAndSigner — 1/1 passed
  • Example task simulated end-to-end; diff shows only the two expected
    SystemConfig slots + safe nonce bump

🤖 Generated with Claude Code

Adds a reusable L2TaskBase template for atomically updating batcherHash
and unsafeBlockSigner on SystemConfig in a single batched Multicall3 tx
from FoundationUpgradeSafe. Mirrors the SystemConfigGasParams pattern.

Includes an OP Sepolia example task and a pinned-calldata regression test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@donoso-eth donoso-eth requested review from a team as code owners April 10, 2026 15:50
@donoso-eth donoso-eth requested review from Wazabie and digorithm and removed request for digorithm April 10, 2026 15:50
Copy link
Copy Markdown
Contributor

@Wazabie Wazabie left a comment

Choose a reason for hiding this comment

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

Template Review Findings

Blocking Issues

None found.


Warnings

  • [SetBatcherAndSigner.sol:52-53] No zero-address guards in _templateSetup() for batcherAddress and unsafeBlockSigner. If a zero address is accidentally supplied in config, setBatcherHash would set bytes32(0) and setUnsafeBlockSigner would set address(0), silently breaking sequencing. Consider:

    require(batcherAddress != address(0), "SetBatcherAndSigner: batcherAddress is zero");
    require(unsafeBlockSigner != address(0), "SetBatcherAndSigner: unsafeBlockSigner is zero");
  • [SetBatcherAndSigner.sol:20] Missing /// Supports: NatSpec annotation at the contract level. SystemConfigGasParams (the stated reference template) has /// Supports: anything after Holocene. Please document the supported SystemConfig version range, e.g.:

    /// Supports: any SystemConfig with setBatcherHash + setUnsafeBlockSigner

Passed Checks

  • Interface decouplingISystemConfig defined inline with exactly the 4 methods required; no import from monorepo/@eth-optimism-bedrock
  • Struct field orderingTaskInputs has batcherHash then unsafeBlockSigner (alphabetical). check-struct-order.sh passes with 0 violations
  • forge fmt --check — PASS
  • _templateSetup() calls super._templateSetup() first
  • _build() uses regular function calls (correct for L2TaskBase, not OPCM)
  • _build() writes no global state
  • _validate() iterates all chains and asserts both batcherHash and unsafeBlockSigner slots post-execution
  • _taskStorageWrites() lists SystemConfigProxy + FoundationUpgradeSafe — exact match to SystemConfigGasParams reference
  • _getCodeExceptions() returns batcher + signer addresses as EOA exceptions, correctly justified in comment
  • Example task exists at test/tasks/example/sep/035-set-batcher-and-signer/
  • config.toml uses real chain ID 11155420 (OP Sepolia Testnet); state override correctly sets SystemConfig owner to FoundationUpgradeSafe
  • .env has FORK_BLOCK_NUMBER=10624099
  • NatSpec @notice present on all methods
  • Regression test testRegressionCallDataMatches_SetBatcherAndSigner — 1 passed (with archival RPC)
  • Simulation — succeeded end-to-end; output confirmed below

Simulation

just simulate succeeded with forge v1.1.0 + archival RPC.

State changes — exactly as expected, nothing unexpected:

Contract Slot Before After Description
SystemConfigProxy (0x034edd...b538) 0x67 (batcherHash) (prior value) bytes32(0x1234...5678) New batcher hash
SystemConfigProxy (0x034edd...b538) 0x65a7ed... (unsafeBlockSigner) 0x57CACBB0... 0xabCDeF0123... New unsafe block signer
FoundationUpgradeSafe (0xDEe571...) 0x5 (nonce) 69 70 Safe nonce increment

Tenderly simulation link is in the just simulate output (requires TENDERLY_USERNAME/TENDERLY_PROJECT substitution).

User must verify in Tenderly:

  • Both expected SystemConfig slots changed
  • Safe nonce bumped from 69 → 70
  • No unexpected contracts modified

Audit Trail

Step Command / Action Result
1 ls src/template/ && ls src/tasks/types/ PASS — confirmed in superchain-ops
2 forge --version (v1.1.0 via mise) PASS
3 gh pr view 1393 --json files 4 files identified
4 forge build --deny-warnings FAIL — pre-existing lib/optimism submodule errors, not from this PR
5 forge fmt --check PASS
6 ./src/script/check-struct-order.sh PASS — 0 violations
7 forge test --match-test SetBatcherAndSigner (archival RPC) PASS — 1/1
8 Read SetBatcherAndSigner.sol + SystemConfigGasParams.sol 2 warnings found
9 just simulate sep 035-set-batcher-and-signer (archival RPC, forge v1.1.0) PASS — clean state diff

Verification commands:

forge fmt --check
./src/script/check-struct-order.sh
forge test --match-test testRegressionCallDataMatches_SetBatcherAndSigner

Escalation Checklist — none apply:

  • New template type — No, straightforward L2TaskBase variation of SystemConfigGasParams
  • New OPCM function — No OPCM involved
  • Cross-layer operations — No
  • Security-critical operations — No (sequencer key rotation, not pause/upgrade)
  • Unusual state changes — No, only SystemConfigProxy storage + safe nonce

🤖 Reviewed with superchain-ops-template-review skill

@donoso-eth donoso-eth requested a review from Wazabie April 13, 2026 09:32
Copy link
Copy Markdown
Contributor

@Wazabie Wazabie left a comment

Choose a reason for hiding this comment

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

Manually reviewed and simulated, state changes looks correct.

The major comment I have that needs to be addressed: the template currently hardcodes FUS as the signer which assumes FUS will ALWAYS own SystemConfig at execution time.

We're not yet aligned on whether that's the case, ownership of the SystemConfig in future migrations may be transferred to a new safe we haven't set up yet.

Two possible paths here:

1- Dynamically fetch the owner: change the template to read SystemConfig.owner() and have a config field where to explicitly declare the expected owner address, so the template can assert they match.

Pros: more flexible template, works regardless of who ends up owning SystemConfig
Cons: more complexity in the template and additional failure points.

2- Align on the owner and keep it hardcoded: decide now who will own SystemConfig (FUS or a new safe), update addresses.toml accordingly and document it as a precondition for this template.

Pros: simpler
Cons: needs decision on ownership before this template is actually usable.

Either way, the state override from the example task should be removed once we know the actual owner. Right now it overrides the mismatch and makes the simulation look like it works when it wouldn't on chain and example tasks should reflect as closely as possible the execution in prod environments.

Looping in @sbvegan for visibility

@Wazabie
Copy link
Copy Markdown
Contributor

Wazabie commented Apr 13, 2026

Recap of the checks worth adding, regardless of which path we take on ownership:

1- Zero address guards on batcherAddress and unsafeBlockSigner (in _templateSetup)

2- No-op check: assert that the new values actually differ from the current on chain value. If someone misconfigures the task with the same addresses already set _validate() passes silently.

3- Ownership pre-check: in _templateSetup assert that SystemConfig.owner() matches the expected signer before simulation proceeds. Right now the state override masks this mismatch and the task looks like it works even if the ownership transfer hasn't happened yet.

UPDATE: point 2 (no-ops) should be a broad check that still allows changing only one of the two values

@donoso-eth
Copy link
Copy Markdown
Contributor Author

@Wazabie, as discussed, the override lets us collect signatures in advance.
Regarding:

3- Ownership pre-check: in _templateSetup assert that SystemConfig.owner() matches the expected signer before simulation proceeds. Right now the state override masks this mismatch and the task looks like it works even if the ownership transfer hasn't happened yet.

In the current setup, when simulating it will always pass, and when executing — if the owner condition doesn't match — the template execution will revert anyway because onlyOwner is not satisfied. So in the current setup it doesn't add much value.
That said, it does make sense to simulate as close to reality as possible before broadcasting. One option would be to add a SKIP_STATE_OVERRIDES flag to simulate against current chain state — wdyt?

donoso-eth and others added 2 commits April 13, 2026 16:09
Adds two defensive checks in _templateSetup and makes _build skip
setters whose target value already matches the current on-chain state:

- Reject zero batcherAddress or unsafeBlockSigner in TOML parsing.
- Reject a task where both fields already match the on-chain values
  (OR-semantics, so partial rotations remain valid).
- Precompute per-chain updateBatcher/updateSigner flags in setup and
  call only the setters for fields that actually change, avoiding
  redundant ConfigUpdate events and SSTOREs on partial rotations.

Calldata for the existing example task is unchanged (both fields
differ at the pinned block), so the regression test still passes
byte-identical.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@donoso-eth
Copy link
Copy Markdown
Contributor Author

Added a new commit addressing the review:

  • Reject zero batcherAddress / unsafeBlockSigner at TOML parse time.
  • Reject tasks that would be a full no-op (both fields already match on-chain); partial rotations still allowed.
  • Precompute per-field update flags in _templateSetup; _build only fires setters for fields that actually change

@donoso-eth donoso-eth requested a review from Wazabie April 13, 2026 14:23
@Wazabie
Copy link
Copy Markdown
Contributor

Wazabie commented Apr 14, 2026

@Wazabie, as discussed, the override lets us collect signatures in advance. Regarding:

3- Ownership pre-check: in _templateSetup assert that SystemConfig.owner() matches the expected signer before simulation proceeds. Right now the state override masks this mismatch and the task looks like it works even if the ownership transfer hasn't happened yet.

In the current setup, when simulating it will always pass, and when executing — if the owner condition doesn't match — the template execution will revert anyway because onlyOwner is not satisfied. So in the current setup it doesn't add much value. That said, it does make sense to simulate as close to reality as possible before broadcasting. One option would be to add a SKIP_STATE_OVERRIDES flag to simulate against current chain state — wdyt?

I don't think we need SKIP_STATE_OVERRIDES now. The overrides let us collect signatures in advance and if the ownership condition isn't met at execution time the tx reverts anyway due to onlyOwner.

If you have bandwidth it would be worth updating the template to address my comment 1 here and accept the owner as an input from the config (e.g. a safeAddress field in the TOML) instead of having it hardcoded as "FoundationUpgradeSafe".

@donoso-eth
Copy link
Copy Markdown
Contributor Author

Thanks @Wazabie, both make sense.
Agree on dropping SKIP_STATE_OVERRIDES, onlyOwner already covers execution. I'll queue the configurable-owner refactor as a separate PR.

Copy link
Copy Markdown
Contributor

@Wazabie Wazabie left a comment

Choose a reason for hiding this comment

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

Template Review: SetBatcherAndSigner

Audit Trail

Step Command/Action Result
1 mise exec -- forge build --deny-warnings PASS (0 warnings, 0 errors)
2 mise exec -- forge fmt --check PASS
3 ./src/script/check-struct-order.sh PASS (TaskInputs not TOML-decoded via parseRaw)
4 mise exec -- forge test --skip Integration 210 PASS; 74 FAIL — all stale fork-block RPC errors pre-existing across the repo, unrelated to this template
5 Read src/template/SetBatcherAndSigner.sol 2 warnings found
6 mise exec -- just simulate (sep/035) PASS — script ran successfully

Blocking Issues

None.


Warnings

  • [SetBatcherAndSigner.sol:48–75] _templateSetup() has no vm.label() calls for any key addresses. The batcher address, unsafe block signer, and per-chain SystemConfigProxy addresses should be labeled for Tenderly trace readability.

  • [SetBatcherAndSigner.sol:65–67] The per-chain no-op guard reverts the entire task if any single chain in l2chains already has the target values, even if other chains still need the update. Behavior is intentional but undocumented — worth adding a comment explaining the rationale.


Passed Checks

  • ISystemConfig defined inline — no external interface imports
  • batcherAddress / unsafeBlockSigner cannot be derived from on-chain state (they are the new values)
  • TaskInputs fields alphabetically ordered: batcherHash, unsafeBlockSigner, updateBatcher, updateSigner
  • _templateSetup() calls super._templateSetup() first
  • Non-zero guards on both config inputs
  • _build() uses regular calls — correct for L2TaskBase
  • _build() writes no global state; reads from cfg set in _templateSetup()
  • _validate() iterates all chains and checks both fields — correct: if updateBatcher=false, taskInput.batcherHash equals the current on-chain value, so the assertion passes trivially
  • _taskStorageWrites()SystemConfigProxy + FoundationUpgradeSafe (nonce), neither too broad nor too narrow
  • _getCodeExceptions() correctly returns EOA batcher and signer addresses, sized chains.length * 2
  • Example task at test/tasks/example/sep/035-set-batcher-and-signer/ — 0 missing files
  • config.toml uses real chain ID 11155420 (OP Sepolia Testnet)
  • .env has FORK_BLOCK_NUMBER
  • [stateOverrides] correctly overrides SystemConfig owner to FoundationUpgradeSafe on the Sepolia fork

Simulation — State Changes

Script ran successfully. 2 contracts modified, 0 unexpected.

0x034edD2A225f7f429A63E0f1D2084B9E0A93b538 (SystemConfigProxy) — Chain ID 11155420

Slot Field Before After
0x67 batcherHash (raw bytes32 — state printer renders as empty for non-UTF-8 bytes32; change confirmed by calldata) (raw bytes32)
0x65a7ed5... unsafeBlockSigner 0x57CACBB0d30b01eb2462e5dC940c161aff3230D3 0xAbCdEf0123456789AbCdEf0123456789AbCdEf01

0xDEe57160aAfCF04c34C887B5962D0a69676d3C8B (FoundationUpgradeSafe)

Slot Field Before After
0x05 nonce 69 70

Note on batcherHash display: The state printer tries to render bytes32 as a UTF-8 string and shows empty for non-printable values. The calldata confirms setBatcherHash was included and the slot appeared in the state diff. Pre-existing cosmetic issue in the state printer, not a template bug.


Escalation Checklist

No escalation required. Standard L2TaskBase config-change template — no OPCM, no cross-layer ops, no complex Safe operations.

@Wazabie
Copy link
Copy Markdown
Contributor

Wazabie commented Apr 16, 2026

Tenderly simulation and expected data to sign is consistent with the previous simulation. Will ask for a security review now. cc @Ethnical

@Wazabie
Copy link
Copy Markdown
Contributor

Wazabie commented Apr 20, 2026

@donoso-eth we should update the name of the template to something like and/or (since you can also update one of the two)

Comment thread src/template/SetBatcherAndOrSigner.sol
Comment thread src/template/SetBatcherAndOrSigner.sol
@donoso-eth
Copy link
Copy Markdown
Contributor Author

donoso-eth commented Apr 20, 2026

@donoso-eth we should update the name of the template to something like and/or (since you can also update one of the two)

Thanks @Wazabie, fair point — the name slightly overclaims now that partial rotations are supported.

My lean is to tighten the contract-level NatSpec instead of renaming, to avoid the churn of renaming the contract, file, imports in Regression.t.sol, and the example task reference:

/// @notice Template for updating the batcher hash and/or unsafe block signer on SystemConfig.
/// Either or both fields may be rotated; the task is rejected only when both
/// already match the current on-chain values (full no-op). Eligible setter calls
/// are batched into a single Multicall3 transaction from the SystemConfig owner.
contract SetBatcherAndSigner is L2TaskBase {

Happy to rename if you feel strongly, but wanted to propose the lower-churn option first — wdyt?

Comment thread src/template/SetBatcherAndSigner.sol Outdated
@Ethnical
Copy link
Copy Markdown
Contributor

Super clean!! I just left 1 Nit that we can apply? After this we can merge!

Comment thread src/template/SetBatcherAndSigner.sol Outdated
@Ethnical
Copy link
Copy Markdown
Contributor

@donoso-eth I think we should rename now before the template to show explicitly this is not AND, I am in favor to do this right now and also updating the name of the contract, I feel this is better for clarity tbh!

- Rename contract/file SetBatcherAndSigner -> SetBatcherAndOrSigner to
  reflect that either or both fields can be rotated (partial rotations
  are valid).
- Replace hardcoded "FoundationUpgradeSafe" in _taskStorageWrites with
  safeAddressString() to remove duplication and make the template
  friendlier to a future configurable-owner refactor.
- Tighten _getCodeExceptions NatSpec per review.

Calldata and EIP-712 data-to-sign are unaffected by the rename; the
existing pinned regression test passes byte-identical.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@donoso-eth
Copy link
Copy Markdown
Contributor Author

comments addressed, including renaming.

@donoso-eth donoso-eth requested review from Ethnical and Wazabie April 21, 2026 05:16
@Wazabie Wazabie enabled auto-merge April 21, 2026 06:52
@Wazabie
Copy link
Copy Markdown
Contributor

Wazabie commented Apr 21, 2026

@Ethnical if it looks good we also need one final approval from you!

Copy link
Copy Markdown
Contributor

@Ethnical Ethnical left a comment

Choose a reason for hiding this comment

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

🚀

@Wazabie Wazabie added this pull request to the merge queue Apr 21, 2026
Merged via the queue into main with commit 15b0698 Apr 21, 2026
15 checks passed
@Wazabie Wazabie deleted the jd/template-system-config-batcher-signer branch April 21, 2026 20:14
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