Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
ac5726d
timelock first steps
JereSalo Dec 16, 2025
5e341dd
improve some stuff and add comments
JereSalo Dec 16, 2025
6138e29
add one more comment about security council
JereSalo Dec 16, 2025
a2b631c
Deploy timelock contract and make it compile (not tested)
JereSalo Dec 16, 2025
06429f5
remove comments
JereSalo Dec 16, 2025
75968ed
commit and verify batches to Timelock instead of OnChainProposer
JereSalo Dec 16, 2025
ea206cb
add pause and unpause functions
JereSalo Dec 17, 2025
4e2ab15
change mindelay to 30 seconds for testing
JereSalo Dec 17, 2025
d390d97
add emergency execution for security council
JereSalo Dec 17, 2025
6516788
remove initialization of bridge address and transfer of ownership
JereSalo Dec 17, 2025
3545409
run cargo fmt
JereSalo Dec 17, 2025
6a41d67
fix solidity lint in Timelock
JereSalo Dec 17, 2025
ad29f8a
use onlySelf modifier instead
JereSalo Dec 18, 2025
af1f5d7
merge main
JereSalo Dec 18, 2025
8a8df36
rename error in based contract
JereSalo Dec 18, 2025
a5c12c7
require timelock address to be set if it's not based
JereSalo Dec 18, 2025
9e740c4
make tiny changes
JereSalo Dec 18, 2025
f697e23
deploy timelock only on non-based
JereSalo Dec 18, 2025
e4dd641
improve comments in timelock contract
JereSalo Dec 18, 2025
43589a8
add comments to Timelock contract
JereSalo Dec 18, 2025
1e6c0b2
rename owner to governance
JereSalo Dec 18, 2025
89bc0ba
Merge branch 'main' into timelock_l2
JereSalo Dec 18, 2025
1369951
disable initialize function from TimelockControllerUpgradeable
JereSalo Dec 18, 2025
a16cd29
Merge branch 'timelock_l2' of github.com:lambdaclass/ethrex into time…
JereSalo Dec 18, 2025
7edc1e0
Make Timelock inherit IOnChainProposer
JereSalo Dec 18, 2025
f68872f
let security council upgrade vks
JereSalo Dec 18, 2025
90d7add
try fix timelock bug
JereSalo Dec 19, 2025
21a4d5d
add Clone
JereSalo Dec 19, 2025
f8bce2a
run cargo fmt
JereSalo Dec 19, 2025
fa3c723
make tdx contract point to the timelock and this one implements autho…
JereSalo Dec 19, 2025
3e6b14b
cargo fmt
JereSalo Dec 19, 2025
4442465
Add authorizedSequencerAddresses to interface and implement it as del…
JereSalo Dec 19, 2025
f3b40dd
rename owner to timelock_owner
JereSalo Dec 19, 2025
da63fe6
tidy and improve Timelock contract, create interface
JereSalo Dec 19, 2025
1c33ced
use proper errors in timelock
JereSalo Dec 19, 2025
2681bb2
use let some instead
JereSalo Dec 19, 2025
a56ece8
Keep track of the nonce after deploying
JereSalo Dec 19, 2025
5d8747b
deprecate variable in comment
JereSalo Dec 19, 2025
97f99c0
remove invalid override
JereSalo Dec 19, 2025
0bde3eb
add comment to ocp initialize
JereSalo Dec 19, 2025
d35c56b
Revert "Keep track of the nonce after deploying"
JereSalo Dec 19, 2025
790111f
Merge branch 'main' into timelock_l2
JereSalo Dec 20, 2025
f6e02d7
rename authorizedSequencerAddresses to isSequencer
JereSalo Dec 22, 2025
0b63415
move errors to timelock interface
JereSalo Dec 22, 2025
a02e8e5
ci(l2): fix TDX CI for timelock PR (#5698)
JereSalo Dec 22, 2025
44344bc
add notice
JereSalo Dec 23, 2025
73b8c1c
add address check for bridge
JereSalo Dec 23, 2025
c6fab46
merge main
JereSalo Dec 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cmd/ethrex/build_l2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ pub fn download_script() {
&Path::new("../../crates/l2/contracts/src/l1/Router.sol"),
"Router",
),
(
&Path::new("../../crates/l2/contracts/src/l1/Timelock.sol"),
"Timelock",
),
];
for (path, name) in l1_contracts {
compile_contract_to_bytecode(
Expand Down Expand Up @@ -196,6 +200,7 @@ fn write_empty_bytecode_files(output_contracts_path: &Path) {
"UpgradeableSystemContract",
"SequencerRegistry",
"OnChainProposerBased",
"Timelock",
];

for name in &contract_names {
Expand Down
2 changes: 2 additions & 0 deletions cmd/ethrex/l2/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ impl L2Command {
.sequencer_opts
.committer_opts
.on_chain_proposer_address = Some(contract_addresses.on_chain_proposer_address);
l2_options.sequencer_opts.committer_opts.timelock_address =
contract_addresses.timelock_address;
l2_options.sequencer_opts.watcher_opts.bridge_address =
Some(contract_addresses.bridge_address);
println!("Initializing L2");
Expand Down
222 changes: 108 additions & 114 deletions cmd/ethrex/l2/deployer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -533,17 +533,24 @@ const SEQUENCER_REGISTRY_BYTECODE: &[u8] = include_bytes!(concat!(
"/contracts/solc_out/SequencerRegistry.bytecode"
));

/// Bytecode of the Timelock contract.
/// This is generated by the [build script](./build.rs).
const TIMELOCK_BYTECODE: &[u8] = include_bytes!(concat!(
env!("OUT_DIR"),
"/contracts/solc_out/Timelock.bytecode"
));

/// Bytecode of the SP1Verifier contract.
/// This is generated by the [build script](./build.rs).
const SP1_VERIFIER_BYTECODE: &[u8] = include_bytes!(concat!(
env!("OUT_DIR"),
"/contracts/solc_out/SP1Verifier.bytecode"
));

const INITIALIZE_ON_CHAIN_PROPOSER_SIGNATURE_BASED: &str = "initialize(bool,address,bool,bool,bool,bool,address,address,address,address,bytes32,bytes32,bytes32,bytes32,address,uint256)";
const INITIALIZE_ON_CHAIN_PROPOSER_SIGNATURE: &str = "initialize(bool,address,bool,bool,bool,bool,address,address,address,address,bytes32,bytes32,bytes32,bytes32,address[],uint256)";
const INITIALIZE_ON_CHAIN_PROPOSER_SIGNATURE_BASED: &str = "initialize(bool,address,bool,bool,bool,bool,address,address,address,address,bytes32,bytes32,bytes32,bytes32,address,uint256,address)";
const INITIALIZE_ON_CHAIN_PROPOSER_SIGNATURE: &str = "initialize(bool,address,bool,bool,bool,bool,address,address,address,address,bytes32,bytes32,bytes32,bytes32,uint256,address)";
const INITIALIZE_TIMELOCK_SIGNATURE: &str = "initialize(uint256,address[],address,address,address)";

const INITIALIZE_BRIDGE_ADDRESS_SIGNATURE: &str = "initializeBridgeAddress(address)";
const TRANSFER_OWNERSHIP_SIGNATURE: &str = "transferOwnership(address)";
const ACCEPT_OWNERSHIP_SIGNATURE: &str = "acceptOwnership()";
const BRIDGE_INITIALIZER_SIGNATURE: &str = "initialize(address,address,uint256,address, uint256)";
Expand All @@ -565,6 +572,7 @@ pub struct ContractAddresses {
pub sequencer_registry_address: Address,
pub aligned_aggregator_address: Address,
pub router: Option<Address>,
pub timelock_address: Option<Address>,
}

pub async fn deploy_l1_contracts(
Expand Down Expand Up @@ -716,6 +724,41 @@ async fn deploy_contracts(
Default::default()
};

let (timelock_deployment, timelock_address) = if !opts.deploy_based_contracts {
info!("Deploying Timelock");

let timelock_deployment = deploy_with_proxy_from_bytecode_no_wait(
deployer,
eth_client,
TIMELOCK_BYTECODE,
&salt,
Overrides {
nonce: Some(nonce),
gas_limit: Some(TRANSACTION_GAS_LIMIT),
max_fee_per_gas: Some(gas_price),
max_priority_fee_per_gas: Some(gas_price),
..Default::default()
},
)
.await?;

nonce += 2;

info!(
"Timelock deployed:\n Proxy -> address={:#x}, tx_hash={:#x}\n Impl -> address={:#x}, tx_hash={:#x}",
timelock_deployment.proxy_address,
timelock_deployment.proxy_tx_hash,
timelock_deployment.implementation_address,
timelock_deployment.implementation_tx_hash,
);
(
Some(timelock_deployment.clone()),
Some(timelock_deployment.proxy_address),
)
} else {
(None, None)
};

info!("Deploying OnChainProposer");

trace!("Attempting to deploy OnChainProposer contract");
Expand Down Expand Up @@ -854,13 +897,15 @@ async fn deploy_contracts(
_ => Address::zero(),
};

let tdx_controller_address =
timelock_address.unwrap_or(on_chain_proposer_deployment.proxy_address);

// if it's a required proof type, but no address has been specified, deploy it.
let tdx_verifier_address = match opts.tdx_verifier_address {
Some(addr) if opts.tdx => addr,
None if opts.tdx => {
info!("Deploying TDXVerifier (if tdx_deploy_verifier is true)");
let tdx_verifier_address =
deploy_tdx_contracts(opts, on_chain_proposer_deployment.proxy_address)?;
let tdx_verifier_address = deploy_tdx_contracts(opts, tdx_controller_address)?;

info!(address = %format!("{tdx_verifier_address:#x}"), "TDXVerifier deployed");
tdx_verifier_address
Expand Down Expand Up @@ -888,7 +933,7 @@ async fn deploy_contracts(
tdx_verifier_address = ?tdx_verifier_address,
"Contracts deployed"
);
let receipts = vec![
let mut receipts = vec![
on_chain_proposer_deployment.implementation_tx_hash,
on_chain_proposer_deployment.proxy_tx_hash,
bridge_deployment.implementation_tx_hash,
Expand All @@ -900,6 +945,11 @@ async fn deploy_contracts(
router_deployment.proxy_tx_hash,
];

if let Some(timelock_deployment) = timelock_deployment {
receipts.push(timelock_deployment.implementation_tx_hash);
receipts.push(timelock_deployment.proxy_tx_hash);
}

Ok((
ContractAddresses {
on_chain_proposer_address: on_chain_proposer_deployment.proxy_address,
Expand All @@ -910,6 +960,7 @@ async fn deploy_contracts(
sequencer_registry_address: sequencer_registry_deployment.proxy_address,
aligned_aggregator_address,
router: opts.router.or(Some(router_deployment.proxy_address)),
timelock_address,
},
receipts,
))
Expand Down Expand Up @@ -1033,13 +1084,55 @@ async fn initialize_contracts(
let deployer_address = get_address_from_secret_key(&opts.private_key.secret_bytes())
.map_err(DeployerError::InternalError)?;

if let Some(timelock_address) = contract_addresses.timelock_address {
info!("Initializing Timelock");
let initialize_tx_hash = {
let deployer = Signer::Local(LocalSigner::new(opts.private_key));
let deployer_nonce = eth_client
.get_nonce(deployer_address, BlockIdentifier::Tag(BlockTag::Pending))
.await?;
let calldata_values = vec![
Value::Uint(U256::from(30)), // TODO: Make minDelay parametrizable. For now this is for testing purposes.
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we file an issue for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup, it's important. Created #5703

Value::Array(vec![
// sequencers
Value::Address(opts.committer_l1_address),
Value::Address(opts.proof_sender_l1_address),
]),
Value::Address(opts.on_chain_proposer_owner), // owner
Value::Address(opts.on_chain_proposer_owner), // securityCouncil
Value::Address(contract_addresses.on_chain_proposer_address), // onChainProposer
];
let timelock_initialization_calldata =
encode_calldata(INITIALIZE_TIMELOCK_SIGNATURE, &calldata_values)?;

initialize_contract_no_wait(
timelock_address,
timelock_initialization_calldata,
&deployer,
eth_client,
Overrides {
nonce: Some(deployer_nonce),
gas_limit: Some(TRANSACTION_GAS_LIMIT),
max_fee_per_gas: Some(gas_price),
max_priority_fee_per_gas: Some(gas_price),
..Default::default()
},
)
.await?
};
info!(tx_hash = %format!("{initialize_tx_hash:#x}"), "Timelock initialized");
tx_hashes.push(initialize_tx_hash);
} else {
info!("Skipping Timelock initialization (based enabled)");
}

info!("Initializing OnChainProposer");

if opts.deploy_based_contracts {
// Initialize OnChainProposer with Based config and SequencerRegistry
let calldata_values = vec![
Value::Bool(opts.validium),
Value::Address(deployer_address),
Value::Address(opts.on_chain_proposer_owner),
Value::Bool(opts.risc0),
Value::Bool(opts.sp1),
Value::Bool(opts.tdx),
Expand All @@ -1054,6 +1147,7 @@ async fn initialize_contracts(
Value::FixedBytes(genesis.compute_state_root().0.to_vec().into()),
Value::Address(contract_addresses.sequencer_registry_address),
Value::Uint(genesis.config.chain_id.into()),
Value::Address(contract_addresses.bridge_address),
];

trace!(calldata_values = ?calldata_values, "OnChainProposer initialization calldata values");
Expand Down Expand Up @@ -1149,7 +1243,9 @@ async fn initialize_contracts(
// Initialize only OnChainProposer without Based config
let calldata_values = vec![
Value::Bool(opts.validium),
Value::Address(deployer_address),
Value::Address(contract_addresses.timelock_address.ok_or(
DeployerError::InternalError("Timelock address missing".to_string()),
)?),
Value::Bool(opts.risc0),
Value::Bool(opts.sp1),
Value::Bool(opts.tdx),
Expand All @@ -1162,11 +1258,8 @@ async fn initialize_contracts(
Value::FixedBytes(risc0_vk),
Value::FixedBytes(commit_hash.0.to_vec().into()),
Value::FixedBytes(genesis.compute_state_root().0.to_vec().into()),
Value::Array(vec![
Value::Address(opts.committer_l1_address),
Value::Address(opts.proof_sender_l1_address),
]),
Value::Uint(genesis.config.chain_id.into()),
Value::Address(contract_addresses.bridge_address),
];
trace!(calldata_values = ?calldata_values, "OnChainProposer initialization calldata values");
let on_chain_proposer_initialization_calldata =
Expand Down Expand Up @@ -1196,108 +1289,6 @@ async fn initialize_contracts(
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.

let initializer_nonce = eth_client
.get_nonce(
initializer.address(),
BlockIdentifier::Tag(BlockTag::Pending),
)
.await?;
let calldata_values = vec![Value::Address(contract_addresses.bridge_address)];
let on_chain_proposer_initialization_calldata =
encode_calldata(INITIALIZE_BRIDGE_ADDRESS_SIGNATURE, &calldata_values)?;

initialize_contract_no_wait(
contract_addresses.on_chain_proposer_address,
on_chain_proposer_initialization_calldata,
initializer,
eth_client,
Overrides {
nonce: Some(initializer_nonce),
gas_limit: Some(TRANSACTION_GAS_LIMIT),
max_fee_per_gas: Some(gas_price),
max_priority_fee_per_gas: Some(gas_price),
..Default::default()
},
)
.await?
};

info!(
tx_hash = %format!("{initialize_bridge_address_tx_hash:#x}"),
"OnChainProposer bridge address initialized"
);

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.

let initializer_nonce = eth_client
.get_nonce(
initializer.address(),
BlockIdentifier::Tag(BlockTag::Pending),
)
.await?;
let transfer_ownership_tx_hash = {
let owner_transfer_calldata = encode_calldata(
TRANSFER_OWNERSHIP_SIGNATURE,
&[Value::Address(opts.on_chain_proposer_owner)],
)?;

initialize_contract_no_wait(
contract_addresses.on_chain_proposer_address,
owner_transfer_calldata,
initializer,
eth_client,
Overrides {
nonce: Some(initializer_nonce),
gas_limit: Some(TRANSACTION_GAS_LIMIT),
max_fee_per_gas: Some(gas_price),
max_priority_fee_per_gas: Some(gas_price),
..Default::default()
},
)
.await?
};

tx_hashes.push(transfer_ownership_tx_hash);

if let Some(owner_pk) = opts.on_chain_proposer_owner_pk {
let signer = Signer::Local(LocalSigner::new(owner_pk));
let owner_nonce = eth_client
.get_nonce(signer.address(), BlockIdentifier::Tag(BlockTag::Pending))
.await?;
let accept_ownership_calldata = encode_calldata(ACCEPT_OWNERSHIP_SIGNATURE, &[])?;
let accept_tx = build_generic_tx(
eth_client,
TxType::EIP1559,
contract_addresses.on_chain_proposer_address,
opts.on_chain_proposer_owner,
accept_ownership_calldata.into(),
Overrides {
nonce: Some(owner_nonce),
gas_limit: Some(TRANSACTION_GAS_LIMIT),
max_fee_per_gas: Some(gas_price),
max_priority_fee_per_gas: Some(gas_price),
..Default::default()
},
)
.await?;
let accept_tx_hash = send_generic_transaction(eth_client, accept_tx, &signer).await?;
tx_hashes.push(accept_tx_hash);

info!(
transfer_tx_hash = %format!("{transfer_ownership_tx_hash:#x}"),
accept_tx_hash = %format!("{accept_tx_hash:#x}"),
"OnChainProposer ownership transfered"
);
} else {
info!(
transfer_tx_hash = %format!("{transfer_ownership_tx_hash:#x}"),
"OnChainProposer ownership transfered but not accepted yet"
);
}
}

info!("Initializing CommonBridge");
let initialize_tx_hash = {
let initializer_nonce = eth_client
Expand Down Expand Up @@ -1581,6 +1572,9 @@ fn write_contract_addresses_to_env(
"ETHREX_COMMITTER_ON_CHAIN_PROPOSER_ADDRESS={:#x}",
contract_addresses.on_chain_proposer_address
)?;
if let Some(timelock_address) = contract_addresses.timelock_address {
writeln!(writer, "ETHREX_TIMELOCK_ADDRESS={:#x}", timelock_address)?;
}
writeln!(
writer,
"ETHREX_WATCHER_BRIDGE_ADDRESS={:#x}",
Expand Down
10 changes: 10 additions & 0 deletions cmd/ethrex/l2/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ impl TryFrom<SequencerOptions> for SequencerConfig {
.committer_opts
.on_chain_proposer_address
.ok_or(SequencerOptionsError::NoOnChainProposerAddress)?,
timelock_address: opts.committer_opts.timelock_address,
first_wake_up_time_ms: opts.committer_opts.first_wake_up_time_ms.unwrap_or(0),
commit_time_ms: opts.committer_opts.commit_time_ms,
batch_gas_limit: opts.committer_opts.batch_gas_limit,
Expand Down Expand Up @@ -605,6 +606,13 @@ pub struct CommitterOptions {
required_unless_present = "dev"
)]
pub on_chain_proposer_address: Option<Address>,
#[arg(
long = "l1.timelock-address",
value_name = "ADDRESS",
env = "ETHREX_TIMELOCK_ADDRESS",
help_heading = "L1 Committer options"
)]
pub timelock_address: Option<Address>,
#[arg(
long = "committer.commit-time",
default_value = "60000",
Expand Down Expand Up @@ -648,6 +656,7 @@ impl Default for CommitterOptions {
)
.ok(),
on_chain_proposer_address: None,
timelock_address: None,
commit_time_ms: 60000,
batch_gas_limit: None,
first_wake_up_time_ms: None,
Expand Down Expand Up @@ -675,6 +684,7 @@ impl CommitterOptions {
self.on_chain_proposer_address = self
.on_chain_proposer_address
.or(defaults.on_chain_proposer_address);
self.timelock_address = self.timelock_address.or(defaults.timelock_address);
self.batch_gas_limit = self.batch_gas_limit.or(defaults.batch_gas_limit);
self.first_wake_up_time_ms = self
.first_wake_up_time_ms
Expand Down
Loading
Loading