Skip to content

Conversation

@JereSalo
Copy link
Contributor

@JereSalo JereSalo commented Dec 17, 2025

Motivation

  • Add timelock contract that will manage OnChainProposer

Description

  • Timelock contract will be the owner of the OnChainProposer contract. Every transaction should go to the Timelock and this will call the OnChainProposer. Via schedule/execute, emergencyExecute in case of security council or commit/verify in case of sequencers.
  • Despite it being a "Timelock" it actually manages access to functions that don't have timelock logic as well. Like pausing the contract. It has logic for managing roles, so for example in the case of pausing/unpausing the OnChainProposer only the SecurityCouncil will be able to do that instantly. Otherwise the governance of the Timelock will be able to do it but respecting the minimum delay.
  • Remove transfer of ownership in OnChainProposer and directly set the final owner in initialize() and at the same time initialize the bridge contract because we already have it's address as it has already been deployed.
  • Had to update the TDXVerifier to point to the timelock because this one is the one that manages permissions, so it knows what sequencers are valid and which ones are not.

@github-actions
Copy link

github-actions bot commented Dec 17, 2025

Lines of code report

Total lines added: 27
Total lines removed: 9
Total lines changed: 36

Detailed view
+--------------------------------------------+-------+------+
| File                                       | Lines | Diff |
+--------------------------------------------+-------+------+
| ethrex/cmd/ethrex/build_l2.rs              | 447   | +5   |
+--------------------------------------------+-------+------+
| ethrex/cmd/ethrex/l2/command.rs            | 647   | +2   |
+--------------------------------------------+-------+------+
| ethrex/cmd/ethrex/l2/deployer.rs           | 1481  | -9   |
+--------------------------------------------+-------+------+
| ethrex/cmd/ethrex/l2/options.rs            | 1051  | +10  |
+--------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/configs.rs      | 105   | +1   |
+--------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/l1_committer.rs | 1280  | +9   |
+--------------------------------------------+-------+------+

tx_hashes.push(initialize_tx_hash);
}

let initialize_bridge_address_tx_hash = {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We now initialize the bridge in the initialize() of the OnChainProposer, this is not necessary anymore.


tx_hashes.push(initialize_bridge_address_tx_hash);

if opts.on_chain_proposer_owner != initializer.address() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We don't need to transfer ownership anymore, we now initialize the ownership correctly in initialize() instead of transferring it later.

// Logic for updating Timelock contract. Should be triggered by the timelock itself so that it respects min time.
function _authorizeUpgrade(
address /*newImplementation*/
) internal view override {
Copy link
Contributor

Choose a reason for hiding this comment

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

Use an onlySelf modifier here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done ad29f8a

@JereSalo JereSalo marked this pull request as ready for review December 18, 2025 14:59
Copilot AI review requested due to automatic review settings December 18, 2025 14:59
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a Timelock contract to manage the OnChainProposer contract, providing an exit window for users before critical operations take effect. The Timelock implements role-based access control with SEQUENCER and SECURITY_COUNCIL roles, where sequencers can commit and verify batches, and the security council can perform emergency operations without delay.

Key Changes

  • New Timelock contract that acts as an intermediary between sequencers and the OnChainProposer, with support for role-based access control and emergency operations
  • OnChainProposer initialization now includes bridge address setup, eliminating the need for a separate initializeBridgeAddress() call and the subsequent ownership transfer process
  • Rust sequencer components updated to route transactions through the Timelock address (for non-based deployments) instead of directly to the OnChainProposer

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
crates/l2/contracts/src/l1/Timelock.sol New upgradeable Timelock contract implementing TimelockControllerUpgradeable with role-based access for sequencer operations and security council emergency functions
crates/l2/contracts/src/l1/OnChainProposer.sol Removed sequencer authorization mapping and modifier; changed access control to onlyOwner (expected to be Timelock); integrated bridge initialization into main initialize()
crates/l2/contracts/src/l1/based/OnChainProposer.sol Similar access control changes as non-based version; bridge initialization moved into main initialize() with validation
crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol Removed initializeBridgeAddress() function from interface as it's no longer needed
crates/l2/contracts/src/l1/based/interfaces/IOnChainProposer.sol Removed initializeBridgeAddress() function from based interface
crates/l2/sequencer/l1_proof_verifier.rs Added timelock_address field; transactions now routed to timelock_address if present, falling back to on_chain_proposer_address
crates/l2/sequencer/l1_proof_sender.rs Added timelock_address field with same routing logic as proof verifier
crates/l2/sequencer/l1_committer.rs Added timelock_address field; for non-based deployments requires timelock_address; based deployments use on_chain_proposer_address directly
crates/l2/sequencer/configs.rs Added timelock_address Option field to CommitterConfig
cmd/ethrex/l2/options.rs Added timelock-address CLI option and environment variable support for committer configuration
cmd/ethrex/l2/deployer.rs Added Timelock deployment and initialization; updated OnChainProposer initialization signatures to include bridge parameter; removed separate bridge initialization and ownership transfer logic
cmd/ethrex/l2/command.rs Updated dev mode initialization to set timelock_address from deployed contracts
cmd/ethrex/build_l2.rs Added Timelock contract to the list of contracts to compile

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

contract Timelock is
TimelockControllerUpgradeable,
UUPSUpgradeable,
IOnChainProposer
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This can be reverted if necessary, I implemented IOnChainProposer in order to inherit its docs and to interact with the contract in a more direct manner. The other path is to remove the functions and call them only through schedule, execute and emergencyExecute and the Timelock contract would have way less functions

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think it's a good idea to inherit IOnChainProposer. It seems confusing

@JereSalo
Copy link
Contributor Author

JereSalo commented Dec 19, 2025

Current Status: Based workflow wasn't finishing to run, it got stuck in a loop. I'm trying to fix it by just having timelock logic if it's non-based and in based leave everything as it was.

Edit: this was fixed, now TDX test failed. Maybe it was because of flakiness, I'll check that out.

Comment on lines +755 to +756
Some(timelock_deployment.clone()),
Some(timelock_deployment.proxy_address),
Copy link
Contributor

Choose a reason for hiding this comment

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

Why does this return both deployment and address (being the last a field of the former)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I use the address multiple times then and it's best to have it in a timelock_address variable that's an option. Otherwise I have to do .map(|t| t.proxy_address) more than once unnecessarily. This way I just have the variable for direct use and it's simpler

Comment on lines 1085 to 1091
if !opts.deploy_based_contracts {
let timelock_address =
contract_addresses
.timelock_address
.ok_or(DeployerError::InternalError(
"Timelock address missing".to_string(),
))?;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd go for

if let Some(timelock_address) = contract_addresses.timelock_address

Instead of checking based

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done 2681bb2

&deployer,
eth_client,
Overrides {
nonce: Some(deployer_nonce),
Copy link
Contributor

Choose a reason for hiding this comment

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

The nonce should be kept across initializers if no_wait is used

Copy link
Contributor Author

@JereSalo JereSalo Dec 19, 2025

Choose a reason for hiding this comment

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

If I understand you correctly you mean that we should manually increase the nonce. Before I believe it was working fine (for based) because we were using get_block Pending though. But it's true that this may be flaky if you send them too fast maybe. I made some changes in this commit, let me know if I got it wrong
a56ece8

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, I reverted it for now because clippy was complaining and I didn't think it was worth to handle that but we can check what the best thing to do is later!

Comment on lines 72 to 74
/// @dev The sequencer addresses that are authorized to commit and verify batches.
mapping(address _authorizedAddress => bool)
public authorizedSequencerAddresses;
Copy link
Contributor

Choose a reason for hiding this comment

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

We can't delete this, we need to mark it as deprecated in a comment

Copy link
Contributor Author

Choose a reason for hiding this comment

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

5d8747b
You are right, didn't contemplate the case of already deployed OnChainProposers

Copy link
Contributor

Choose a reason for hiding this comment

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

We should add our ITimelock interface

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added

/// @param target The address that was called.
/// @param value The ETH value that was sent.
/// @param data The calldata that was forwarded to `target`.
event EmergencyExecution(address indexed target, uint256 value, bytes data);
Copy link
Contributor

Choose a reason for hiding this comment

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

Move this event to the interface

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done


/// @dev Restricts calls to the timelock itself.
modifier onlySelf() {
require(
Copy link
Contributor

Choose a reason for hiding this comment

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

Use custom errors instead of strings

Copy link
Contributor Author

Choose a reason for hiding this comment

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

address[] memory,
address
) public pure override {
revert("Timelock: use the custom initialize function");
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 87 to 95
/// @inheritdoc IOnChainProposer
function lastCommittedBatch() external view returns (uint256) {
return onChainProposer.lastCommittedBatch();
}

/// @inheritdoc IOnChainProposer
function lastVerifiedBatch() external view returns (uint256) {
return onChainProposer.lastVerifiedBatch();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I wouldn't add view functions in the timelock

Copy link
Contributor Author

@JereSalo JereSalo Dec 19, 2025

Choose a reason for hiding this comment

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

Removed them now that the Timelock is no longer IOnChainProposer

}

/// @inheritdoc IOnChainProposer
/// @custom:access Restricted to accounts with the `SECURITY_COUNCIL` role.
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need all this functions having emergencyExecute()?

Copy link
Contributor Author

@JereSalo JereSalo Dec 19, 2025

Choose a reason for hiding this comment

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

Edit: I removed IOnChainProposer so I removed all these functions as well because of that

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L2 Rollup client

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

4 participants