diff --git a/crates/mega-evm/benches/block_bench.rs b/crates/mega-evm/benches/block_bench.rs index 3b1ffea..0cd9ae9 100644 --- a/crates/mega-evm/benches/block_bench.rs +++ b/crates/mega-evm/benches/block_bench.rs @@ -85,9 +85,8 @@ fn all_hardforks_config() -> MegaHardforkConfig { .with(MegaHardfork::Rex4, ForkCondition::Timestamp(0)) .with(MegaHardfork::Rex5, ForkCondition::Timestamp(0)) .with_params(SequencerRegistryConfig { - initial_system_address: MEGA_SYSTEM_ADDRESS, - initial_sequencer: MEGA_SYSTEM_ADDRESS, - initial_admin: MEGA_SYSTEM_ADDRESS, + rex5_initial_sequencer: MEGA_SYSTEM_ADDRESS, + rex5_initial_admin: MEGA_SYSTEM_ADDRESS, }) } diff --git a/crates/mega-evm/src/block/hardfork.rs b/crates/mega-evm/src/block/hardfork.rs index 51f9ef0..c137a8a 100644 --- a/crates/mega-evm/src/block/hardfork.rs +++ b/crates/mega-evm/src/block/hardfork.rs @@ -462,13 +462,12 @@ mod tests { #[test] fn test_fork_params_typed_access() { let params = SequencerRegistryConfig { - initial_system_address: alloy_primitives::address!( - "0x1111111111111111111111111111111111111111" - ), - initial_sequencer: alloy_primitives::address!( + rex5_initial_sequencer: alloy_primitives::address!( "0x2222222222222222222222222222222222222222" ), - initial_admin: alloy_primitives::address!("0x3333333333333333333333333333333333333333"), + rex5_initial_admin: alloy_primitives::address!( + "0x3333333333333333333333333333333333333333" + ), }; let config = MegaHardforkConfig::default() diff --git a/crates/mega-evm/src/system/sequencer_registry.rs b/crates/mega-evm/src/system/sequencer_registry.rs index c907e2c..cc44709 100644 --- a/crates/mega-evm/src/system/sequencer_registry.rs +++ b/crates/mega-evm/src/system/sequencer_registry.rs @@ -50,34 +50,35 @@ pub use mega_system_contracts::sequencer_registry::ISequencerRegistry; /// Bootstrap configuration for `SequencerRegistry` (attached to Rex5 via [`HardforkParams`]). /// -/// All three addresses are required. There is no `Default`. +/// The initial system address is fixed to [`MEGA_SYSTEM_ADDRESS`] at genesis and is not +/// configurable: pre-Rex5 components (payload executor, txpool, replay) all assume the +/// system tx sender equals the legacy constant, and a configurable initial value would +/// silently break those invariants. After Rex5 activation, the system address can be +/// rotated via the registry's scheduling/apply flow. +/// +/// Field names carry an explicit `rex5_` prefix because these values seed the +/// `SequencerRegistry` only when Rex5 activates; pre-Rex5 blocks read the system address +/// from the legacy constant and ignore this struct entirely. #[derive(Debug, Clone, PartialEq, Eq)] pub struct SequencerRegistryConfig { - /// The initial system address (Oracle/system-tx sender). - pub initial_system_address: Address, - /// The initial sequencer (mini-block signing key). - pub initial_sequencer: Address, - /// The initial admin for the registry. - pub initial_admin: Address, + /// The initial sequencer (mini-block signing key) seeded at Rex5 activation. + pub rex5_initial_sequencer: Address, + /// The initial admin for the registry, seeded at Rex5 activation. + pub rex5_initial_admin: Address, } impl HardforkParams for SequencerRegistryConfig { const FORK: MegaHardfork = MegaHardfork::Rex5; fn validate(&self) -> Result<(), HardforkParamsError> { - if self.initial_system_address.is_zero() { + if self.rex5_initial_sequencer.is_zero() { return Err(HardforkParamsError { - message: "SequencerRegistryConfig.initial_system_address must not be zero".into(), + message: "SequencerRegistryConfig.rex5_initial_sequencer must not be zero".into(), }); } - if self.initial_sequencer.is_zero() { + if self.rex5_initial_admin.is_zero() { return Err(HardforkParamsError { - message: "SequencerRegistryConfig.initial_sequencer must not be zero".into(), - }); - } - if self.initial_admin.is_zero() { - return Err(HardforkParamsError { - message: "SequencerRegistryConfig.initial_admin must not be zero".into(), + message: "SequencerRegistryConfig.rex5_initial_admin must not be zero".into(), }); } Ok(()) @@ -184,9 +185,11 @@ pub fn transact_deploy_sequencer_registry( revm_acc.mark_created(); // Seed initial storage (flat slots only, no dynamic arrays). - let initial_system_address = address_to_storage_value(config.initial_system_address); - let initial_sequencer = address_to_storage_value(config.initial_sequencer); - let initial_admin = address_to_storage_value(config.initial_admin); + // The initial system address is fixed to MEGA_SYSTEM_ADDRESS at genesis — see + // [`SequencerRegistryConfig`] for rationale. + let initial_system_address = address_to_storage_value(MEGA_SYSTEM_ADDRESS); + let initial_sequencer = address_to_storage_value(config.rex5_initial_sequencer); + let initial_admin = address_to_storage_value(config.rex5_initial_admin); let initial_from_block = U256::from(current_block_number); for (slot, value) in [ @@ -359,19 +362,22 @@ pub fn resolve_system_address( mod tests { use super::*; use alloy_primitives::{address, keccak256, B256}; + use mega_system_contracts::sequencer_registry::storage_slots::PENDING_ADMIN; use revm::{context::BlockEnv, database::InMemoryDB, state::AccountInfo}; use crate::{MegaHardforkConfig, MegaSpecId}; - const TEST_SYSTEM_ADDRESS: Address = address!("0xA887dCB9D5f39Ef79272801d05Abdf707CFBbD1d"); const TEST_SEQUENCER: Address = address!("0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); const TEST_ADMIN: Address = address!("0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"); + /// A non-system test address, used as a stand-in for a `_currentSystemAddress` that has + /// already been rotated away from the genesis `MEGA_SYSTEM_ADDRESS`. Distinct from + /// genesis so tests can tell the two apart. + const TEST_SYSTEM_ADDRESS: Address = address!("0xA887dCB9D5f39Ef79272801d05Abdf707CFBbD1d"); fn test_config() -> SequencerRegistryConfig { SequencerRegistryConfig { - initial_system_address: TEST_SYSTEM_ADDRESS, - initial_sequencer: TEST_SEQUENCER, - initial_admin: TEST_ADMIN, + rex5_initial_sequencer: TEST_SEQUENCER, + rex5_initial_admin: TEST_ADMIN, } } @@ -393,17 +399,23 @@ mod tests { assert_eq!(CURRENT_SYSTEM_ADDRESS, U256::from(0), "_currentSystemAddress = slot 0"); assert_eq!(CURRENT_SEQUENCER, U256::from(1), "_currentSequencer = slot 1"); assert_eq!(ADMIN, U256::from(2), "_admin = slot 2"); - assert_eq!(INITIAL_SYSTEM_ADDRESS, U256::from(3), "_initialSystemAddress = slot 3"); - assert_eq!(INITIAL_SEQUENCER, U256::from(4), "_initialSequencer = slot 4"); - assert_eq!(INITIAL_FROM_BLOCK, U256::from(5), "_initialFromBlock = slot 5"); - assert_eq!(PENDING_SYSTEM_ADDRESS, U256::from(6), "_pendingSystemAddress = slot 6"); + assert_eq!(PENDING_ADMIN, U256::from(3), "_pendingAdmin = slot 3"); + assert_eq!(INITIAL_SYSTEM_ADDRESS, U256::from(4), "_initialSystemAddress = slot 4"); + assert_eq!(INITIAL_SEQUENCER, U256::from(5), "_initialSequencer = slot 5"); + assert_eq!(INITIAL_FROM_BLOCK, U256::from(6), "_initialFromBlock = slot 6"); + assert_eq!(PENDING_SYSTEM_ADDRESS, U256::from(7), "_pendingSystemAddress = slot 7"); assert_eq!( SYSTEM_ADDRESS_ACTIVATION_BLOCK, - U256::from(7), - "_systemAddressActivationBlock = slot 7" + U256::from(8), + "_systemAddressActivationBlock = slot 8" ); - assert_eq!(PENDING_SEQUENCER, U256::from(8), "_pendingSequencer = slot 8"); - assert_eq!(SEQUENCER_ACTIVATION_BLOCK, U256::from(9), "_sequencerActivationBlock = slot 9"); + assert_eq!(PENDING_SEQUENCER, U256::from(9), "_pendingSequencer = slot 9"); + assert_eq!( + SEQUENCER_ACTIVATION_BLOCK, + U256::from(10), + "_sequencerActivationBlock = slot 10" + ); + // Slots 11 (_systemAddressHistory) and 12 (_sequencerHistory) are dynamic arrays. } #[test] @@ -423,36 +435,24 @@ mod tests { } #[test] - fn test_validate_rejects_zero_initial_system_address() { + fn test_validate_rejects_zero_rex5_initial_sequencer() { let mut config = test_config(); - config.initial_system_address = Address::ZERO; - let err = config.validate().expect_err("zero initial_system_address must be rejected"); + config.rex5_initial_sequencer = Address::ZERO; + let err = config.validate().expect_err("zero rex5_initial_sequencer must be rejected"); assert!( - err.message.contains("initial_system_address must not be zero"), + err.message.contains("rex5_initial_sequencer must not be zero"), "unexpected message: {}", err.message, ); } #[test] - fn test_validate_rejects_zero_initial_sequencer() { + fn test_validate_rejects_zero_rex5_initial_admin() { let mut config = test_config(); - config.initial_sequencer = Address::ZERO; - let err = config.validate().expect_err("zero initial_sequencer must be rejected"); + config.rex5_initial_admin = Address::ZERO; + let err = config.validate().expect_err("zero rex5_initial_admin must be rejected"); assert!( - err.message.contains("initial_sequencer must not be zero"), - "unexpected message: {}", - err.message, - ); - } - - #[test] - fn test_validate_rejects_zero_initial_admin() { - let mut config = test_config(); - config.initial_admin = Address::ZERO; - let err = config.validate().expect_err("zero initial_admin must be rejected"); - assert!( - err.message.contains("initial_admin must not be zero"), + err.message.contains("rex5_initial_admin must not be zero"), "unexpected message: {}", err.message, ); @@ -476,7 +476,13 @@ mod tests { assert_eq!( account.storage.get(&CURRENT_SYSTEM_ADDRESS).unwrap().present_value(), - U256::from_be_bytes(TEST_SYSTEM_ADDRESS.into_word().0), + U256::from_be_bytes(MEGA_SYSTEM_ADDRESS.into_word().0), + "initial system address is hardcoded to MEGA_SYSTEM_ADDRESS", + ); + assert_eq!( + account.storage.get(&INITIAL_SYSTEM_ADDRESS).unwrap().present_value(), + U256::from_be_bytes(MEGA_SYSTEM_ADDRESS.into_word().0), + "_initialSystemAddress is hardcoded to MEGA_SYSTEM_ADDRESS", ); assert_eq!( account.storage.get(&CURRENT_SEQUENCER).unwrap().present_value(), diff --git a/crates/mega-evm/tests/block_executor/sequencer_registry.rs b/crates/mega-evm/tests/block_executor/sequencer_registry.rs index 85ff440..462c3be 100644 --- a/crates/mega-evm/tests/block_executor/sequencer_registry.rs +++ b/crates/mega-evm/tests/block_executor/sequencer_registry.rs @@ -13,15 +13,15 @@ use alloy_op_evm::block::receipt_builder::OpAlloyReceiptBuilder; use alloy_primitives::{address, Address, Bytes, Signature, TxKind, B256, U256}; use alloy_sol_types::SolCall; use mega_evm::{ - test_utils::MemoryDatabase, BlockLimits, IOracle, MegaBlockExecutionCtx, + test_utils::MemoryDatabase, BlockLimits, IOracle, ISequencerRegistry, MegaBlockExecutionCtx, MegaBlockExecutorFactory, MegaEvmFactory, MegaHardfork, MegaHardforkConfig, MegaSpecId, MegaTxEnvelope, SequencerRegistryConfig, TestExternalEnvs, MEGA_SYSTEM_ADDRESS, ORACLE_CONTRACT_ADDRESS, SEQUENCER_REGISTRY_ADDRESS, SEQUENCER_REGISTRY_CODE, SEQUENCER_REGISTRY_CODE_HASH, }; use mega_system_contracts::sequencer_registry::storage_slots::{ - CURRENT_SEQUENCER, CURRENT_SYSTEM_ADDRESS, PENDING_SEQUENCER, PENDING_SYSTEM_ADDRESS, - SEQUENCER_ACTIVATION_BLOCK, SYSTEM_ADDRESS_ACTIVATION_BLOCK, + ADMIN, CURRENT_SEQUENCER, CURRENT_SYSTEM_ADDRESS, PENDING_ADMIN, PENDING_SEQUENCER, + PENDING_SYSTEM_ADDRESS, SEQUENCER_ACTIVATION_BLOCK, SYSTEM_ADDRESS_ACTIVATION_BLOCK, }; use revm::{ context::BlockEnv, @@ -33,12 +33,12 @@ use revm::{ const NEW_SYSTEM_ADDRESS: Address = address!("3000000000000000000000000000000000000003"); const BOOTSTRAP_SEQUENCER: Address = address!("0x4000000000000000000000000000000000000004"); const BOOTSTRAP_ADMIN: Address = address!("0x5000000000000000000000000000000000000005"); +const NEW_ADMIN: Address = address!("0x6000000000000000000000000000000000000006"); fn sequencer_registry_config() -> SequencerRegistryConfig { SequencerRegistryConfig { - initial_system_address: MEGA_SYSTEM_ADDRESS, - initial_sequencer: BOOTSTRAP_SEQUENCER, - initial_admin: BOOTSTRAP_ADMIN, + rex5_initial_sequencer: BOOTSTRAP_SEQUENCER, + rex5_initial_admin: BOOTSTRAP_ADMIN, } } @@ -74,10 +74,33 @@ fn create_tx_from( alloy_consensus::transaction::Recovered::new_unchecked(tx, sender) } +/// Like [`create_tx_from`] but with an explicit `gas_limit`. System-address senders bypass +/// block gas-limit validation, so existing tests can use the very-large 1B figure; +/// regular-EOA tests must respect the block gas limit (30M in `create_evm_env`). +fn create_tx_from_with_gas_limit( + sender: Address, + nonce: u64, + target: Address, + data: Bytes, + gas_limit: u64, +) -> alloy_consensus::transaction::Recovered { + let tx_legacy = TxLegacy { + chain_id: Some(8453), + nonce, + gas_price: 0, + gas_limit, + to: TxKind::Call(target), + value: U256::ZERO, + input: data, + }; + let signed = Signed::new_unchecked(tx_legacy, Signature::test_signature(), Default::default()); + let tx = MegaTxEnvelope::Legacy(signed); + alloy_consensus::transaction::Recovered::new_unchecked(tx, sender) +} + /// After `apply_pre_execution_changes()`, the context's `system_address` should be -/// the `initial_system_address` from `SequencerRegistryConfig` because the -/// `SequencerRegistry` was -/// just deployed with that value seeded into storage. +/// `MEGA_SYSTEM_ADDRESS` because `SequencerRegistry` is deployed at genesis with the +/// initial system address hardcoded to that constant. #[test] fn test_bootstrap_block_resolves_system_address() { let mut db = MemoryDatabase::default(); @@ -107,8 +130,8 @@ fn test_bootstrap_block_resolves_system_address() { let system_address = executor.evm().ctx_ref().system_address(); assert_eq!( system_address, MEGA_SYSTEM_ADDRESS, - "Bootstrap block should resolve system_address to \ - SequencerRegistryConfig.initial_system_address" + "Bootstrap block should resolve system_address to MEGA_SYSTEM_ADDRESS \ + (the genesis-hardcoded initial system address)" ); } @@ -494,3 +517,103 @@ fn test_missing_sequencer_sequencer_registry_config_errors() { "unexpected error: {err}" ); } + +/// End-to-end exercise of the two-step admin handoff through the block executor: the current +/// admin submits `transferAdmin`, the new admin submits `acceptAdmin`, and the registry's +/// `_admin` slot is promoted while `_pendingAdmin` is cleared. Mirrors the pattern used by +/// `test_system_address_change` for the system-address rotation flow. +#[test] +fn test_admin_handoff_via_block_executor() { + let mut db = MemoryDatabase::default(); + db.set_account_balance(MEGA_SYSTEM_ADDRESS, U256::from(1_000_000_000_000_000u64)); + db.set_account_balance(BOOTSTRAP_ADMIN, U256::from(1_000_000_000_000_000u64)); + db.set_account_balance(NEW_ADMIN, U256::from(1_000_000_000_000_000u64)); + + db.insert_account_info( + SEQUENCER_REGISTRY_ADDRESS, + AccountInfo { + code_hash: SEQUENCER_REGISTRY_CODE_HASH, + code: Some(Bytecode::new_raw(SEQUENCER_REGISTRY_CODE)), + ..Default::default() + }, + ); + // Seed the slots the handoff path actually reads/writes (slot 0 keeps Oracle/system-address + // resolution happy across `apply_pre_execution_changes`; slot 2 is the modifier guard). + db.insert_account_storage( + SEQUENCER_REGISTRY_ADDRESS, + CURRENT_SYSTEM_ADDRESS, + MEGA_SYSTEM_ADDRESS.into_word().into(), + ) + .unwrap(); + db.insert_account_storage( + SEQUENCER_REGISTRY_ADDRESS, + ADMIN, + BOOTSTRAP_ADMIN.into_word().into(), + ) + .unwrap(); + + let mut state = State::builder().with_database(&mut db).build(); + let external_envs = TestExternalEnvs::::new(); + let evm_factory = MegaEvmFactory::new().with_external_env_factory(external_envs); + let chain_spec = MegaHardforkConfig::default() + .with(MegaHardfork::Rex5, ForkCondition::Timestamp(0)) + .with_params(sequencer_registry_config()); + let receipt_builder = OpAlloyReceiptBuilder::default(); + let block_executor_factory = + MegaBlockExecutorFactory::new(chain_spec, evm_factory, receipt_builder); + + let block_ctx = MegaBlockExecutionCtx::new( + B256::ZERO, + Some(B256::ZERO), + Bytes::new(), + BlockLimits::no_limits(), + ); + + let mut executor = + block_executor_factory.create_executor(&mut state, block_ctx, create_evm_env()); + executor.apply_pre_execution_changes().expect("pre-execution changes should succeed"); + + // tx#1: current admin schedules a transfer to NEW_ADMIN. + let calldata = ISequencerRegistry::transferAdminCall { newAdmin: NEW_ADMIN }.abi_encode(); + let tx = create_tx_from_with_gas_limit( + BOOTSTRAP_ADMIN, + 0, + SEQUENCER_REGISTRY_ADDRESS, + Bytes::from(calldata), + 1_000_000, + ); + let receipt = executor + .execute_transaction(&tx) + .expect("transferAdmin tx should be accepted by the executor"); + assert!(receipt > 0, "transferAdmin tx should report non-zero gas used"); + + // tx#2: NEW_ADMIN completes the handoff. + let calldata = ISequencerRegistry::acceptAdminCall {}.abi_encode(); + let tx = create_tx_from_with_gas_limit( + NEW_ADMIN, + 0, + SEQUENCER_REGISTRY_ADDRESS, + Bytes::from(calldata), + 1_000_000, + ); + let receipt = executor + .execute_transaction(&tx) + .expect("acceptAdmin tx should be accepted by the executor"); + assert!(receipt > 0, "acceptAdmin tx should report non-zero gas used"); + + // Drop the executor to release the &mut borrow on `state`, then read the post-state. + drop(executor); + + let admin_slot = revm::Database::storage(&mut state, SEQUENCER_REGISTRY_ADDRESS, ADMIN) + .expect("read ADMIN slot"); + assert_eq!( + admin_slot, + U256::from_be_bytes(NEW_ADMIN.into_word().0), + "_admin must be promoted to NEW_ADMIN after acceptAdmin", + ); + + let pending_slot = + revm::Database::storage(&mut state, SEQUENCER_REGISTRY_ADDRESS, PENDING_ADMIN) + .expect("read PENDING_ADMIN slot"); + assert_eq!(pending_slot, U256::ZERO, "_pendingAdmin must be cleared after acceptAdmin"); +} diff --git a/crates/mega-evm/tests/rex5/apply_pending_changes_gas_budget.rs b/crates/mega-evm/tests/rex5/apply_pending_changes_gas_budget.rs index b2759e2..a55e63e 100644 --- a/crates/mega-evm/tests/rex5/apply_pending_changes_gas_budget.rs +++ b/crates/mega-evm/tests/rex5/apply_pending_changes_gas_budget.rs @@ -87,9 +87,8 @@ impl BucketHasher for SingleBucketHasher { fn sequencer_registry_config() -> SequencerRegistryConfig { SequencerRegistryConfig { - initial_system_address: MEGA_SYSTEM_ADDRESS, - initial_sequencer: BOOTSTRAP_SEQUENCER, - initial_admin: BOOTSTRAP_ADMIN, + rex5_initial_sequencer: BOOTSTRAP_SEQUENCER, + rex5_initial_admin: BOOTSTRAP_ADMIN, } } diff --git a/crates/system-contracts/artifacts/SequencerRegistry-1.0.0.json b/crates/system-contracts/artifacts/SequencerRegistry-1.0.0.json index f6f9d23..94c5d27 100644 --- a/crates/system-contracts/artifacts/SequencerRegistry-1.0.0.json +++ b/crates/system-contracts/artifacts/SequencerRegistry-1.0.0.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "bytecodeLength": 3664, - "codeHash": "0x2dd91bc339d4dadc8cec5a7096213af7cacb02bbbd97308e168564ee5357fb65", - "deployedBytecode": "0x608060405234801561000f575f5ffd5b50600436106100b9575f3560e01c806375829def11610072578063e3093f5011610058578063e3093f5014610199578063e8129030146101ac578063f851a440146101ca575f5ffd5b806375829def14610169578063de7a36af1461017c575f5ffd5b80633411f3c3116100a25780633411f3c31461010f57806344d744591461012257806354fd4d501461012a575f5ffd5b8063129c8240146100bd5780632b20d6e5146100fa575b5f5ffd5b6100d06100cb366004610cc2565b6101e8565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020015b60405180910390f35b61010d610108366004610d01565b61030e565b005b61010d61011d366004610d01565b6105c0565b61010d610864565b604080518082018252600581527f312e302e30000000000000000000000000000000000000000000000000000000602082015290516100f19190610d29565b61010d610177366004610d7c565b610876565b5f5473ffffffffffffffffffffffffffffffffffffffff166100d0565b6100d06101a7366004610cc2565b610963565b60015473ffffffffffffffffffffffffffffffffffffffff166100d0565b60025473ffffffffffffffffffffffffffffffffffffffff166100d0565b5f43821115610223576040517fdbe289ad00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60055482101561025f576040517fd2c461b000000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600b54805b80156102ed575f600b610278600184610dc9565b8154811061028857610288610de2565b5f91825260209091200180549091506bffffffffffffffffffffffff1685106102da57546c01000000000000000000000000900473ffffffffffffffffffffffffffffffffffffffff16949350505050565b50806102e581610e0f565b915050610264565b505060045473ffffffffffffffffffffffffffffffffffffffff1692915050565b610316610a89565b43811161034f576040517fc2fdad0400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff810361047e5773ffffffffffffffffffffffffffffffffffffffff8216156103c4576040517fd92e233d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600880547fffffffffffffffffffffffff00000000000000000000000000000000000000001690555f600981905560015473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f7ac0520c61d87c77ffc308e23c801f5a351140175ec3f6356a3a890aa0cfdbf97fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60405161047291815260200190565b60405180910390a35050565b6bffffffffffffffffffffffff8111156104c4576040517f9d7378d300000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216610511576040517fd92e233d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6008805473ffffffffffffffffffffffffffffffffffffffff84167fffffffffffffffffffffffff00000000000000000000000000000000000000009091168117909155600982905561057960015473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff167f7ac0520c61d87c77ffc308e23c801f5a351140175ec3f6356a3a890aa0cfdbf98360405161047291815260200190565b6105c8610a89565b438111610601576040517fc2fdad0400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81036107235773ffffffffffffffffffffffffffffffffffffffff821615610676576040517fd92e233d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600680547fffffffffffffffffffffffff00000000000000000000000000000000000000001690555f6007819055805473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167fd839b7efbcae2bd482a7aa1c098265fdb6b43c10e3e5138a8fa2b395a02eb9157fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60405161047291815260200190565b6bffffffffffffffffffffffff811115610769576040517f9d7378d300000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82166107b6576040517fd92e233d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6006805473ffffffffffffffffffffffffffffffffffffffff84167fffffffffffffffffffffffff00000000000000000000000000000000000000009091168117909155600782905561081d5f5473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff167fd839b7efbcae2bd482a7aa1c098265fdb6b43c10e3e5138a8fa2b395a02eb9158360405161047291815260200190565b61086c610ada565b610874610bcf565b565b61087e610a89565b73ffffffffffffffffffffffffffffffffffffffff81166108cb576040517fd92e233d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f6108eb60025473ffffffffffffffffffffffffffffffffffffffff1690565b600280547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff85811691821790925560405192935091908316907ff8ccb027dfcd135e000e9d45e6cc2d662578a8825d4c45b5e32e0adf67e79ec6905f90a35050565b5f4382111561099e576040517fdbe289ad00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6005548210156109da576040517fd2c461b000000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600a54805b8015610a68575f600a6109f3600184610dc9565b81548110610a0357610a03610de2565b5f91825260209091200180549091506bffffffffffffffffffffffff168510610a5557546c01000000000000000000000000900473ffffffffffffffffffffffffffffffffffffffff16949350505050565b5080610a6081610e0f565b9150506109df565b505060035473ffffffffffffffffffffffffffffffffffffffff1692915050565b60025473ffffffffffffffffffffffffffffffffffffffff163314610874576040517f7bfa4b9f00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60065473ffffffffffffffffffffffffffffffffffffffff1680610afb5750565b60075443811115610b0a575050565b5f805473ffffffffffffffffffffffffffffffffffffffff9384167fffffffffffffffffffffffff000000000000000000000000000000000000000091821681178355604080518082019091526bffffffffffffffffffffffff948516815260208101918252600a8054600181018255908552905191519095166c01000000000000000000000000029316929092177fc65a7bb8d6351c1cf70c95a316cc6a92839c986682d98bc35f958f4883f9d2a890930192909255600680549091169055600755565b60085473ffffffffffffffffffffffffffffffffffffffff1680610bf05750565b60095443811115610bff575050565b6001805473ffffffffffffffffffffffffffffffffffffffff9384167fffffffffffffffffffffffff000000000000000000000000000000000000000091821681178355604080518082019091526bffffffffffffffffffffffff948516815260208101918252600b805494850181555f908152905191519095166c01000000000000000000000000029316929092177f0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db990910155600880549091169055600955565b5f60208284031215610cd2575f5ffd5b5035919050565b803573ffffffffffffffffffffffffffffffffffffffff81168114610cfc575f5ffd5b919050565b5f5f60408385031215610d12575f5ffd5b610d1b83610cd9565b946020939093013593505050565b602081525f82518060208401528060208501604085015e5f6040828501015260407fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011684010191505092915050565b5f60208284031215610d8c575f5ffd5b610d9582610cd9565b9392505050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b81810381811115610ddc57610ddc610d9c565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b5f81610e1d57610e1d610d9c565b507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff019056fea164736f6c634300081e000a" + "bytecodeLength": 3874, + "codeHash": "0x63cd411a379be1c198613ef1d15c3058e7b0db4a5d07d4bcf07014af90040315", + "deployedBytecode": "0x608060405234801561000f575f5ffd5b50600436106100cf575f3560e01c806354fd4d501161007d578063e3093f5011610058578063e3093f50146101d5578063e8129030146101e8578063f851a44014610206575f5ffd5b806354fd4d501461016657806375829def146101a5578063de7a36af146101b8575f5ffd5b80632b20d6e5116100ad5780632b20d6e5146101385780633411f3c31461014b57806344d744591461015e575f5ffd5b80630e18b681146100d3578063129c8240146100dd578063267822471461011a575b5f5ffd5b6100db610224565b005b6100f06100eb366004610d94565b6102f7565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020015b60405180910390f35b60035473ffffffffffffffffffffffffffffffffffffffff166100f0565b6100db610146366004610dd3565b61041d565b6100db610159366004610dd3565b6106cf565b6100db610973565b604080518082018252600581527f312e302e30000000000000000000000000000000000000000000000000000000602082015290516101119190610dfb565b6100db6101b3366004610e4e565b610985565b5f5473ffffffffffffffffffffffffffffffffffffffff166100f0565b6100f06101e3366004610d94565b610a35565b60015473ffffffffffffffffffffffffffffffffffffffff166100f0565b60025473ffffffffffffffffffffffffffffffffffffffff166100f0565b60035473ffffffffffffffffffffffffffffffffffffffff16338114610276576040517f058d9a1b00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6002805473ffffffffffffffffffffffffffffffffffffffff8381167fffffffffffffffffffffffff00000000000000000000000000000000000000008084168217909455600380549094169093556040519116919082907ff8ccb027dfcd135e000e9d45e6cc2d662578a8825d4c45b5e32e0adf67e79ec6905f90a35050565b5f43821115610332576040517fdbe289ad00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60065482101561036e576040517fd2c461b000000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600c54805b80156103fc575f600c610387600184610e9b565b8154811061039757610397610eb4565b5f91825260209091200180549091506bffffffffffffffffffffffff1685106103e957546c01000000000000000000000000900473ffffffffffffffffffffffffffffffffffffffff16949350505050565b50806103f481610ee1565b915050610373565b505060055473ffffffffffffffffffffffffffffffffffffffff1692915050565b610425610b5b565b43811161045e576040517fc2fdad0400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff810361058d5773ffffffffffffffffffffffffffffffffffffffff8216156104d3576040517fd92e233d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600980547fffffffffffffffffffffffff00000000000000000000000000000000000000001690555f600a81905560015473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f7ac0520c61d87c77ffc308e23c801f5a351140175ec3f6356a3a890aa0cfdbf97fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60405161058191815260200190565b60405180910390a35050565b6bffffffffffffffffffffffff8111156105d3576040517f9d7378d300000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216610620576040517fd92e233d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6009805473ffffffffffffffffffffffffffffffffffffffff84167fffffffffffffffffffffffff00000000000000000000000000000000000000009091168117909155600a82905561068860015473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff167f7ac0520c61d87c77ffc308e23c801f5a351140175ec3f6356a3a890aa0cfdbf98360405161058191815260200190565b6106d7610b5b565b438111610710576040517fc2fdad0400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81036108325773ffffffffffffffffffffffffffffffffffffffff821615610785576040517fd92e233d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600780547fffffffffffffffffffffffff00000000000000000000000000000000000000001690555f6008819055805473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167fd839b7efbcae2bd482a7aa1c098265fdb6b43c10e3e5138a8fa2b395a02eb9157fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60405161058191815260200190565b6bffffffffffffffffffffffff811115610878576040517f9d7378d300000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82166108c5576040517fd92e233d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6007805473ffffffffffffffffffffffffffffffffffffffff84167fffffffffffffffffffffffff00000000000000000000000000000000000000009091168117909155600882905561092c5f5473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff167fd839b7efbcae2bd482a7aa1c098265fdb6b43c10e3e5138a8fa2b395a02eb9158360405161058191815260200190565b61097b610bac565b610983610ca1565b565b61098d610b5b565b6003805473ffffffffffffffffffffffffffffffffffffffff83167fffffffffffffffffffffffff000000000000000000000000000000000000000090911681179091556109f060025473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff167fe5cd1c804f1c9cc6d7009e4c0fb532f0e2d8863524c3323a6b3790c3f80bf25c60405160405180910390a350565b5f43821115610a70576040517fdbe289ad00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600654821015610aac576040517fd2c461b000000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600b54805b8015610b3a575f600b610ac5600184610e9b565b81548110610ad557610ad5610eb4565b5f91825260209091200180549091506bffffffffffffffffffffffff168510610b2757546c01000000000000000000000000900473ffffffffffffffffffffffffffffffffffffffff16949350505050565b5080610b3281610ee1565b915050610ab1565b505060045473ffffffffffffffffffffffffffffffffffffffff1692915050565b60025473ffffffffffffffffffffffffffffffffffffffff163314610983576040517f7bfa4b9f00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60075473ffffffffffffffffffffffffffffffffffffffff1680610bcd5750565b60085443811115610bdc575050565b5f805473ffffffffffffffffffffffffffffffffffffffff9384167fffffffffffffffffffffffff000000000000000000000000000000000000000091821681178355604080518082019091526bffffffffffffffffffffffff948516815260208101918252600b8054600181018255908552905191519095166c01000000000000000000000000029316929092177f0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db990930192909255600780549091169055600855565b60095473ffffffffffffffffffffffffffffffffffffffff1680610cc25750565b600a5443811115610cd1575050565b6001805473ffffffffffffffffffffffffffffffffffffffff9384167fffffffffffffffffffffffff000000000000000000000000000000000000000091821681178355604080518082019091526bffffffffffffffffffffffff948516815260208101918252600c805494850181555f908152905191519095166c01000000000000000000000000029316929092177fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c790910155600980549091169055600a55565b5f60208284031215610da4575f5ffd5b5035919050565b803573ffffffffffffffffffffffffffffffffffffffff81168114610dce575f5ffd5b919050565b5f5f60408385031215610de4575f5ffd5b610ded83610dab565b946020939093013593505050565b602081525f82518060208401528060208501604085015e5f6040828501015260407fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011684010191505092915050565b5f60208284031215610e5e575f5ffd5b610e6782610dab565b9392505050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b81810381811115610eae57610eae610e6e565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b5f81610eef57610eef610e6e565b507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff019056fea164736f6c634300081e000a" } \ No newline at end of file diff --git a/crates/system-contracts/contracts/SequencerRegistry.sol b/crates/system-contracts/contracts/SequencerRegistry.sol index 20047a1..0448ae3 100644 --- a/crates/system-contracts/contracts/SequencerRegistry.sol +++ b/crates/system-contracts/contracts/SequencerRegistry.sol @@ -19,6 +19,10 @@ contract SequencerRegistry is ISemver, ISequencerRegistry { /// @notice The admin that can schedule role changes and transfer admin ownership. address private _admin; + /// @notice The address allowed to call `acceptAdmin()` to complete a two-step admin transfer, + /// or `address(0)` if no transfer is pending. + address private _pendingAdmin; + /// @notice The bootstrap system address returned before the first system address change. address private _initialSystemAddress; @@ -193,6 +197,11 @@ contract SequencerRegistry is ISemver, ISequencerRegistry { return _admin; } + /// @inheritdoc ISequencerRegistry + function pendingAdmin() public view returns (address) { + return _pendingAdmin; + } + /// @dev Reverts if msg.sender is not the current admin. modifier onlyAdmin() { _onlyAdmin(); @@ -204,12 +213,23 @@ contract SequencerRegistry is ISemver, ISequencerRegistry { } /// @inheritdoc ISequencerRegistry + /// @dev Two-step transfer: sets `_pendingAdmin` and does NOT change `_admin`. The new admin + /// becomes effective only when they call `acceptAdmin()`. Passing `address(0)` cancels + /// any previously pending transfer. A subsequent call overwrites the pending slot. function transferAdmin(address newAdmin) external onlyAdmin { - if (newAdmin == address(0)) revert ZeroAddress(); + _pendingAdmin = newAdmin; + emit AdminTransferStarted(admin(), newAdmin); + } + + /// @inheritdoc ISequencerRegistry + function acceptAdmin() external { + address pending = _pendingAdmin; + if (msg.sender != pending) revert NotPendingAdmin(); - address oldAdmin = admin(); - _admin = newAdmin; + address oldAdmin = _admin; + _admin = pending; + delete _pendingAdmin; - emit AdminTransferred(oldAdmin, newAdmin); + emit AdminTransferred(oldAdmin, pending); } } diff --git a/crates/system-contracts/contracts/interfaces/ISequencerRegistry.sol b/crates/system-contracts/contracts/interfaces/ISequencerRegistry.sol index e09f029..4eded1b 100644 --- a/crates/system-contracts/contracts/interfaces/ISequencerRegistry.sol +++ b/crates/system-contracts/contracts/interfaces/ISequencerRegistry.sol @@ -21,6 +21,9 @@ interface ISequencerRegistry { /// @notice Thrown when the caller is not the current admin. error NotAdmin(); + /// @notice Thrown when the caller is not the pending admin. + error NotPendingAdmin(); + /// @notice Thrown when a zero address is passed where a non-zero address is required. error ZeroAddress(); @@ -71,12 +74,28 @@ interface ISequencerRegistry { /// @notice Applies all pending role changes that are due. Called as a pre-block system call. function applyPendingChanges() external; - /// @notice Emitted when the admin is transferred. + /// @notice Emitted when the current admin starts a two-step transfer by setting a pending admin. + /// @dev Also emitted with `newPendingAdmin == address(0)` when the current admin cancels a + /// pending transfer. + event AdminTransferStarted(address indexed currentAdmin, address indexed newPendingAdmin); + + /// @notice Emitted when the pending admin accepts the transfer and becomes the new admin. event AdminTransferred(address indexed oldAdmin, address indexed newAdmin); /// @notice Returns the current admin address. function admin() external view returns (address); - /// @notice Transfers admin to a new address. + /// @notice Returns the address that is currently allowed to call `acceptAdmin`, or + /// `address(0)` if no transfer is pending. + function pendingAdmin() external view returns (address); + + /// @notice Sets the pending admin to `newAdmin`. The current admin remains in effect until + /// `newAdmin` calls `acceptAdmin()`. Pass `address(0)` to cancel a pending transfer. + /// @dev Two-step transfer (sets pending; does not change `admin()` immediately). + /// A subsequent call overwrites any previously pending admin. function transferAdmin(address newAdmin) external; + + /// @notice Completes a two-step admin transfer. Must be called by the address previously + /// passed to `transferAdmin`. Sets `admin()` to the caller and clears the pending slot. + function acceptAdmin() external; } diff --git a/crates/system-contracts/src/generated/sequencer_registry_artifacts.rs b/crates/system-contracts/src/generated/sequencer_registry_artifacts.rs index 26b85ea..08689e7 100644 --- a/crates/system-contracts/src/generated/sequencer_registry_artifacts.rs +++ b/crates/system-contracts/src/generated/sequencer_registry_artifacts.rs @@ -4,9 +4,9 @@ use alloy_primitives::{bytes, b256, Bytes, B256}; /// `SequencerRegistry` contract bytecode v1.0.0 -pub const V1_0_0_CODE: Bytes = bytes!("608060405234801561000f575f5ffd5b50600436106100b9575f3560e01c806375829def11610072578063e3093f5011610058578063e3093f5014610199578063e8129030146101ac578063f851a440146101ca575f5ffd5b806375829def14610169578063de7a36af1461017c575f5ffd5b80633411f3c3116100a25780633411f3c31461010f57806344d744591461012257806354fd4d501461012a575f5ffd5b8063129c8240146100bd5780632b20d6e5146100fa575b5f5ffd5b6100d06100cb366004610cc2565b6101e8565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020015b60405180910390f35b61010d610108366004610d01565b61030e565b005b61010d61011d366004610d01565b6105c0565b61010d610864565b604080518082018252600581527f312e302e30000000000000000000000000000000000000000000000000000000602082015290516100f19190610d29565b61010d610177366004610d7c565b610876565b5f5473ffffffffffffffffffffffffffffffffffffffff166100d0565b6100d06101a7366004610cc2565b610963565b60015473ffffffffffffffffffffffffffffffffffffffff166100d0565b60025473ffffffffffffffffffffffffffffffffffffffff166100d0565b5f43821115610223576040517fdbe289ad00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60055482101561025f576040517fd2c461b000000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600b54805b80156102ed575f600b610278600184610dc9565b8154811061028857610288610de2565b5f91825260209091200180549091506bffffffffffffffffffffffff1685106102da57546c01000000000000000000000000900473ffffffffffffffffffffffffffffffffffffffff16949350505050565b50806102e581610e0f565b915050610264565b505060045473ffffffffffffffffffffffffffffffffffffffff1692915050565b610316610a89565b43811161034f576040517fc2fdad0400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff810361047e5773ffffffffffffffffffffffffffffffffffffffff8216156103c4576040517fd92e233d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600880547fffffffffffffffffffffffff00000000000000000000000000000000000000001690555f600981905560015473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f7ac0520c61d87c77ffc308e23c801f5a351140175ec3f6356a3a890aa0cfdbf97fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60405161047291815260200190565b60405180910390a35050565b6bffffffffffffffffffffffff8111156104c4576040517f9d7378d300000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216610511576040517fd92e233d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6008805473ffffffffffffffffffffffffffffffffffffffff84167fffffffffffffffffffffffff00000000000000000000000000000000000000009091168117909155600982905561057960015473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff167f7ac0520c61d87c77ffc308e23c801f5a351140175ec3f6356a3a890aa0cfdbf98360405161047291815260200190565b6105c8610a89565b438111610601576040517fc2fdad0400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81036107235773ffffffffffffffffffffffffffffffffffffffff821615610676576040517fd92e233d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600680547fffffffffffffffffffffffff00000000000000000000000000000000000000001690555f6007819055805473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167fd839b7efbcae2bd482a7aa1c098265fdb6b43c10e3e5138a8fa2b395a02eb9157fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60405161047291815260200190565b6bffffffffffffffffffffffff811115610769576040517f9d7378d300000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82166107b6576040517fd92e233d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6006805473ffffffffffffffffffffffffffffffffffffffff84167fffffffffffffffffffffffff00000000000000000000000000000000000000009091168117909155600782905561081d5f5473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff167fd839b7efbcae2bd482a7aa1c098265fdb6b43c10e3e5138a8fa2b395a02eb9158360405161047291815260200190565b61086c610ada565b610874610bcf565b565b61087e610a89565b73ffffffffffffffffffffffffffffffffffffffff81166108cb576040517fd92e233d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f6108eb60025473ffffffffffffffffffffffffffffffffffffffff1690565b600280547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff85811691821790925560405192935091908316907ff8ccb027dfcd135e000e9d45e6cc2d662578a8825d4c45b5e32e0adf67e79ec6905f90a35050565b5f4382111561099e576040517fdbe289ad00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6005548210156109da576040517fd2c461b000000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600a54805b8015610a68575f600a6109f3600184610dc9565b81548110610a0357610a03610de2565b5f91825260209091200180549091506bffffffffffffffffffffffff168510610a5557546c01000000000000000000000000900473ffffffffffffffffffffffffffffffffffffffff16949350505050565b5080610a6081610e0f565b9150506109df565b505060035473ffffffffffffffffffffffffffffffffffffffff1692915050565b60025473ffffffffffffffffffffffffffffffffffffffff163314610874576040517f7bfa4b9f00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60065473ffffffffffffffffffffffffffffffffffffffff1680610afb5750565b60075443811115610b0a575050565b5f805473ffffffffffffffffffffffffffffffffffffffff9384167fffffffffffffffffffffffff000000000000000000000000000000000000000091821681178355604080518082019091526bffffffffffffffffffffffff948516815260208101918252600a8054600181018255908552905191519095166c01000000000000000000000000029316929092177fc65a7bb8d6351c1cf70c95a316cc6a92839c986682d98bc35f958f4883f9d2a890930192909255600680549091169055600755565b60085473ffffffffffffffffffffffffffffffffffffffff1680610bf05750565b60095443811115610bff575050565b6001805473ffffffffffffffffffffffffffffffffffffffff9384167fffffffffffffffffffffffff000000000000000000000000000000000000000091821681178355604080518082019091526bffffffffffffffffffffffff948516815260208101918252600b805494850181555f908152905191519095166c01000000000000000000000000029316929092177f0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db990910155600880549091169055600955565b5f60208284031215610cd2575f5ffd5b5035919050565b803573ffffffffffffffffffffffffffffffffffffffff81168114610cfc575f5ffd5b919050565b5f5f60408385031215610d12575f5ffd5b610d1b83610cd9565b946020939093013593505050565b602081525f82518060208401528060208501604085015e5f6040828501015260407fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011684010191505092915050565b5f60208284031215610d8c575f5ffd5b610d9582610cd9565b9392505050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b81810381811115610ddc57610ddc610d9c565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b5f81610e1d57610e1d610d9c565b507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff019056fea164736f6c634300081e000a"); +pub const V1_0_0_CODE: Bytes = bytes!("608060405234801561000f575f5ffd5b50600436106100cf575f3560e01c806354fd4d501161007d578063e3093f5011610058578063e3093f50146101d5578063e8129030146101e8578063f851a44014610206575f5ffd5b806354fd4d501461016657806375829def146101a5578063de7a36af146101b8575f5ffd5b80632b20d6e5116100ad5780632b20d6e5146101385780633411f3c31461014b57806344d744591461015e575f5ffd5b80630e18b681146100d3578063129c8240146100dd578063267822471461011a575b5f5ffd5b6100db610224565b005b6100f06100eb366004610d94565b6102f7565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020015b60405180910390f35b60035473ffffffffffffffffffffffffffffffffffffffff166100f0565b6100db610146366004610dd3565b61041d565b6100db610159366004610dd3565b6106cf565b6100db610973565b604080518082018252600581527f312e302e30000000000000000000000000000000000000000000000000000000602082015290516101119190610dfb565b6100db6101b3366004610e4e565b610985565b5f5473ffffffffffffffffffffffffffffffffffffffff166100f0565b6100f06101e3366004610d94565b610a35565b60015473ffffffffffffffffffffffffffffffffffffffff166100f0565b60025473ffffffffffffffffffffffffffffffffffffffff166100f0565b60035473ffffffffffffffffffffffffffffffffffffffff16338114610276576040517f058d9a1b00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6002805473ffffffffffffffffffffffffffffffffffffffff8381167fffffffffffffffffffffffff00000000000000000000000000000000000000008084168217909455600380549094169093556040519116919082907ff8ccb027dfcd135e000e9d45e6cc2d662578a8825d4c45b5e32e0adf67e79ec6905f90a35050565b5f43821115610332576040517fdbe289ad00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60065482101561036e576040517fd2c461b000000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600c54805b80156103fc575f600c610387600184610e9b565b8154811061039757610397610eb4565b5f91825260209091200180549091506bffffffffffffffffffffffff1685106103e957546c01000000000000000000000000900473ffffffffffffffffffffffffffffffffffffffff16949350505050565b50806103f481610ee1565b915050610373565b505060055473ffffffffffffffffffffffffffffffffffffffff1692915050565b610425610b5b565b43811161045e576040517fc2fdad0400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff810361058d5773ffffffffffffffffffffffffffffffffffffffff8216156104d3576040517fd92e233d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600980547fffffffffffffffffffffffff00000000000000000000000000000000000000001690555f600a81905560015473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f7ac0520c61d87c77ffc308e23c801f5a351140175ec3f6356a3a890aa0cfdbf97fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60405161058191815260200190565b60405180910390a35050565b6bffffffffffffffffffffffff8111156105d3576040517f9d7378d300000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216610620576040517fd92e233d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6009805473ffffffffffffffffffffffffffffffffffffffff84167fffffffffffffffffffffffff00000000000000000000000000000000000000009091168117909155600a82905561068860015473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff167f7ac0520c61d87c77ffc308e23c801f5a351140175ec3f6356a3a890aa0cfdbf98360405161058191815260200190565b6106d7610b5b565b438111610710576040517fc2fdad0400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81036108325773ffffffffffffffffffffffffffffffffffffffff821615610785576040517fd92e233d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600780547fffffffffffffffffffffffff00000000000000000000000000000000000000001690555f6008819055805473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167fd839b7efbcae2bd482a7aa1c098265fdb6b43c10e3e5138a8fa2b395a02eb9157fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60405161058191815260200190565b6bffffffffffffffffffffffff811115610878576040517f9d7378d300000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82166108c5576040517fd92e233d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6007805473ffffffffffffffffffffffffffffffffffffffff84167fffffffffffffffffffffffff00000000000000000000000000000000000000009091168117909155600882905561092c5f5473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff167fd839b7efbcae2bd482a7aa1c098265fdb6b43c10e3e5138a8fa2b395a02eb9158360405161058191815260200190565b61097b610bac565b610983610ca1565b565b61098d610b5b565b6003805473ffffffffffffffffffffffffffffffffffffffff83167fffffffffffffffffffffffff000000000000000000000000000000000000000090911681179091556109f060025473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff167fe5cd1c804f1c9cc6d7009e4c0fb532f0e2d8863524c3323a6b3790c3f80bf25c60405160405180910390a350565b5f43821115610a70576040517fdbe289ad00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600654821015610aac576040517fd2c461b000000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600b54805b8015610b3a575f600b610ac5600184610e9b565b81548110610ad557610ad5610eb4565b5f91825260209091200180549091506bffffffffffffffffffffffff168510610b2757546c01000000000000000000000000900473ffffffffffffffffffffffffffffffffffffffff16949350505050565b5080610b3281610ee1565b915050610ab1565b505060045473ffffffffffffffffffffffffffffffffffffffff1692915050565b60025473ffffffffffffffffffffffffffffffffffffffff163314610983576040517f7bfa4b9f00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60075473ffffffffffffffffffffffffffffffffffffffff1680610bcd5750565b60085443811115610bdc575050565b5f805473ffffffffffffffffffffffffffffffffffffffff9384167fffffffffffffffffffffffff000000000000000000000000000000000000000091821681178355604080518082019091526bffffffffffffffffffffffff948516815260208101918252600b8054600181018255908552905191519095166c01000000000000000000000000029316929092177f0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db990930192909255600780549091169055600855565b60095473ffffffffffffffffffffffffffffffffffffffff1680610cc25750565b600a5443811115610cd1575050565b6001805473ffffffffffffffffffffffffffffffffffffffff9384167fffffffffffffffffffffffff000000000000000000000000000000000000000091821681178355604080518082019091526bffffffffffffffffffffffff948516815260208101918252600c805494850181555f908152905191519095166c01000000000000000000000000029316929092177fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c790910155600980549091169055600a55565b5f60208284031215610da4575f5ffd5b5035919050565b803573ffffffffffffffffffffffffffffffffffffffff81168114610dce575f5ffd5b919050565b5f5f60408385031215610de4575f5ffd5b610ded83610dab565b946020939093013593505050565b602081525f82518060208401528060208501604085015e5f6040828501015260407fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011684010191505092915050565b5f60208284031215610e5e575f5ffd5b610e6782610dab565b9392505050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b81810381811115610eae57610eae610e6e565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b5f81610eef57610eef610e6e565b507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff019056fea164736f6c634300081e000a"); /// `SequencerRegistry` contract code hash v1.0.0 -pub const V1_0_0_CODE_HASH: B256 = b256!("2dd91bc339d4dadc8cec5a7096213af7cacb02bbbd97308e168564ee5357fb65"); +pub const V1_0_0_CODE_HASH: B256 = b256!("63cd411a379be1c198613ef1d15c3058e7b0db4a5d07d4bcf07014af90040315"); /// Latest `SequencerRegistry` contract bytecode pub const LATEST_CODE: Bytes = V1_0_0_CODE; diff --git a/crates/system-contracts/src/lib.rs b/crates/system-contracts/src/lib.rs index 46503b1..691d793 100644 --- a/crates/system-contracts/src/lib.rs +++ b/crates/system-contracts/src/lib.rs @@ -88,25 +88,43 @@ pub mod sequencer_registry { /// Storage slot for `_admin`. pub const ADMIN: U256 = U256::from_limbs([2, 0, 0, 0]); + /// Storage slot for `_pendingAdmin`. + pub const PENDING_ADMIN: U256 = U256::from_limbs([3, 0, 0, 0]); + /// Storage slot for `_initialSystemAddress`. - pub const INITIAL_SYSTEM_ADDRESS: U256 = U256::from_limbs([3, 0, 0, 0]); + pub const INITIAL_SYSTEM_ADDRESS: U256 = U256::from_limbs([4, 0, 0, 0]); /// Storage slot for `_initialSequencer`. - pub const INITIAL_SEQUENCER: U256 = U256::from_limbs([4, 0, 0, 0]); + pub const INITIAL_SEQUENCER: U256 = U256::from_limbs([5, 0, 0, 0]); /// Storage slot for `_initialFromBlock`. - pub const INITIAL_FROM_BLOCK: U256 = U256::from_limbs([5, 0, 0, 0]); + pub const INITIAL_FROM_BLOCK: U256 = U256::from_limbs([6, 0, 0, 0]); /// Storage slot for `_pendingSystemAddress`. - pub const PENDING_SYSTEM_ADDRESS: U256 = U256::from_limbs([6, 0, 0, 0]); + pub const PENDING_SYSTEM_ADDRESS: U256 = U256::from_limbs([7, 0, 0, 0]); /// Storage slot for `_systemAddressActivationBlock`. - pub const SYSTEM_ADDRESS_ACTIVATION_BLOCK: U256 = U256::from_limbs([7, 0, 0, 0]); + pub const SYSTEM_ADDRESS_ACTIVATION_BLOCK: U256 = U256::from_limbs([8, 0, 0, 0]); /// Storage slot for `_pendingSequencer`. - pub const PENDING_SEQUENCER: U256 = U256::from_limbs([8, 0, 0, 0]); + pub const PENDING_SEQUENCER: U256 = U256::from_limbs([9, 0, 0, 0]); /// Storage slot for `_sequencerActivationBlock`. - pub const SEQUENCER_ACTIVATION_BLOCK: U256 = U256::from_limbs([9, 0, 0, 0]); + pub const SEQUENCER_ACTIVATION_BLOCK: U256 = U256::from_limbs([10, 0, 0, 0]); + + // Slots 11 (`_systemAddressHistory`) and 12 (`_sequencerHistory`) are Solidity + // dynamic arrays. By the standard layout, the slot itself stores `array.length` and + // element `i` lives at `keccak256(bytes32(slot)) + i`. Concretely: + // + // `_systemAddressHistory[0]` is at + // `keccak256(bytes32(uint256(11)))` + // = `0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db9` + // `_sequencerHistory[0]` is at + // `keccak256(bytes32(uint256(12)))` + // = `0xdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c7` + // + // Rust never reads or writes these arrays directly — `systemAddressAt` and + // `sequencerAt` are on-chain getters that iterate them — so there is no flat slot + // constant. } } diff --git a/crates/system-contracts/test/Oracle.t.sol b/crates/system-contracts/test/Oracle.t.sol index 515169d..a460a29 100644 --- a/crates/system-contracts/test/Oracle.t.sol +++ b/crates/system-contracts/test/Oracle.t.sol @@ -28,12 +28,20 @@ contract OracleTest is Test { registry = SequencerRegistry(REGISTRY_ADDRESS); // Seed SequencerRegistry storage (no constructor). + // Slot layout (kept in sync with SequencerRegistry.t.sol::setUp): + // 0: _currentSystemAddress + // 1: _currentSequencer + // 2: _admin + // 3: _pendingAdmin (left at zero — no pending transfer at bootstrap) + // 4: _initialSystemAddress + // 5: _initialSequencer + // 6: _initialFromBlock vm.store(REGISTRY_ADDRESS, bytes32(uint256(0)), bytes32(uint256(uint160(INITIAL_SYSTEM_ADDRESS)))); vm.store(REGISTRY_ADDRESS, bytes32(uint256(1)), bytes32(uint256(uint160(INITIAL_SEQUENCER)))); vm.store(REGISTRY_ADDRESS, bytes32(uint256(2)), bytes32(uint256(uint160(INITIAL_ADMIN)))); - vm.store(REGISTRY_ADDRESS, bytes32(uint256(3)), bytes32(uint256(uint160(INITIAL_SYSTEM_ADDRESS)))); - vm.store(REGISTRY_ADDRESS, bytes32(uint256(4)), bytes32(uint256(uint160(INITIAL_SEQUENCER)))); - vm.store(REGISTRY_ADDRESS, bytes32(uint256(5)), bytes32(uint256(INITIAL_FROM_BLOCK))); + vm.store(REGISTRY_ADDRESS, bytes32(uint256(4)), bytes32(uint256(uint160(INITIAL_SYSTEM_ADDRESS)))); + vm.store(REGISTRY_ADDRESS, bytes32(uint256(5)), bytes32(uint256(uint160(INITIAL_SEQUENCER)))); + vm.store(REGISTRY_ADDRESS, bytes32(uint256(6)), bytes32(uint256(INITIAL_FROM_BLOCK))); vm.roll(INITIAL_FROM_BLOCK); @@ -41,6 +49,40 @@ contract OracleTest is Test { oracle = new Oracle(); } + // ============ setUp invariants ============ + + /// @notice Verifies that `setUp` seeded `SequencerRegistry` storage at the correct slots. + /// @dev This test reads the bootstrap state through the contract's own view functions, so + /// every slot the fixture writes is exercised end-to-end against the Solidity-defined + /// storage layout. If a future change reorders or renumbers slots without keeping the + /// `vm.store(...)` calls in `setUp` aligned, this test fails — preventing the silent + /// fixture drift that previously hid in plain sight. + function test_setUp_seedsRegistryBootstrapStateCorrectly() public view { + // Slots 0, 1, 2 (current system address / sequencer / admin). + assertEq(registry.admin(), INITIAL_ADMIN, "admin (slot 2)"); + assertEq(registry.currentSystemAddress(), INITIAL_SYSTEM_ADDRESS, "currentSystemAddress (slot 0)"); + assertEq(registry.currentSequencer(), INITIAL_SEQUENCER, "currentSequencer (slot 1)"); + + // Slot 3 (pendingAdmin) must be untouched by the bootstrap fixture. + assertEq(registry.pendingAdmin(), address(0), "pendingAdmin (slot 3) must be zero at bootstrap"); + + // Indirectly exercises `_initialSystemAddress` (slot 4) and `_initialFromBlock` (slot 6): + // `systemAddressAt` reads `_systemAddressHistory` first (empty at bootstrap) and falls + // through to `_initialSystemAddress`; the lower-bound check uses `_initialFromBlock`. + assertEq( + registry.systemAddressAt(INITIAL_FROM_BLOCK), + INITIAL_SYSTEM_ADDRESS, + "systemAddressAt(INITIAL_FROM_BLOCK) covers slots 4 + 6" + ); + + // Indirectly exercises `_initialSequencer` (slot 5) and `_initialFromBlock` (slot 6). + assertEq( + registry.sequencerAt(INITIAL_FROM_BLOCK), + INITIAL_SEQUENCER, + "sequencerAt(INITIAL_FROM_BLOCK) covers slots 5 + 6" + ); + } + // ============ version ============ function test_version() public view { diff --git a/crates/system-contracts/test/SequencerRegistry.t.sol b/crates/system-contracts/test/SequencerRegistry.t.sol index b3d409b..289c9eb 100644 --- a/crates/system-contracts/test/SequencerRegistry.t.sol +++ b/crates/system-contracts/test/SequencerRegistry.t.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import {Test} from "forge-std/Test.sol"; +import {Vm} from "forge-std/Vm.sol"; import {SequencerRegistry} from "../contracts/SequencerRegistry.sol"; import {ISequencerRegistry} from "../contracts/interfaces/ISequencerRegistry.sol"; @@ -31,15 +32,16 @@ contract SequencerRegistryTest is Test { // 0: _currentSystemAddress // 1: _currentSequencer // 2: _admin - // 3: _initialSystemAddress - // 4: _initialSequencer - // 5: _initialFromBlock + // 3: _pendingAdmin (left at zero — no pending transfer at bootstrap) + // 4: _initialSystemAddress + // 5: _initialSequencer + // 6: _initialFromBlock vm.store(REGISTRY_ADDRESS, bytes32(uint256(0)), bytes32(uint256(uint160(INITIAL_SYSTEM_ADDRESS)))); vm.store(REGISTRY_ADDRESS, bytes32(uint256(1)), bytes32(uint256(uint160(INITIAL_SEQUENCER)))); vm.store(REGISTRY_ADDRESS, bytes32(uint256(2)), bytes32(uint256(uint160(INITIAL_ADMIN)))); - vm.store(REGISTRY_ADDRESS, bytes32(uint256(3)), bytes32(uint256(uint160(INITIAL_SYSTEM_ADDRESS)))); - vm.store(REGISTRY_ADDRESS, bytes32(uint256(4)), bytes32(uint256(uint160(INITIAL_SEQUENCER)))); - vm.store(REGISTRY_ADDRESS, bytes32(uint256(5)), bytes32(uint256(INITIAL_FROM_BLOCK))); + vm.store(REGISTRY_ADDRESS, bytes32(uint256(4)), bytes32(uint256(uint160(INITIAL_SYSTEM_ADDRESS)))); + vm.store(REGISTRY_ADDRESS, bytes32(uint256(5)), bytes32(uint256(uint160(INITIAL_SEQUENCER)))); + vm.store(REGISTRY_ADDRESS, bytes32(uint256(6)), bytes32(uint256(INITIAL_FROM_BLOCK))); // Start at block >= INITIAL_FROM_BLOCK so lookups work. vm.roll(INITIAL_FROM_BLOCK); @@ -65,20 +67,44 @@ contract SequencerRegistryTest is Test { assertEq(registry.admin(), INITIAL_ADMIN); } - // ============ transferAdmin ============ + function test_setUp_pendingAdminIsZero() public view { + // Slot 3 must be empty at bootstrap; `transferAdmin` is the only way to populate it. + // This test is the symmetric guard to `OracleTest::test_setUp_seedsRegistryBootstrapStateCorrectly`: + // it fails immediately if a future fixture or layout change accidentally writes to slot 3. + assertEq(registry.pendingAdmin(), address(0)); + } + + // ============ transferAdmin (two-step: schedule pending) ============ - function test_transferAdmin_success() public { + function test_transferAdmin_setsPendingButDoesNotChangeAdmin() public { vm.prank(INITIAL_ADMIN); registry.transferAdmin(newAdmin); - assertEq(registry.admin(), newAdmin); + + // Current admin is unchanged until acceptance. + assertEq(registry.admin(), INITIAL_ADMIN); + assertEq(registry.pendingAdmin(), newAdmin); } - function test_transferAdmin_emitsEvent() public { - vm.expectEmit(true, true, false, true); - emit ISequencerRegistry.AdminTransferred(INITIAL_ADMIN, newAdmin); + function test_transferAdmin_emitsAdminTransferStarted() public { + vm.expectEmit(true, true, false, false); + emit ISequencerRegistry.AdminTransferStarted(INITIAL_ADMIN, newAdmin); + + vm.prank(INITIAL_ADMIN); + registry.transferAdmin(newAdmin); + } + function test_transferAdmin_doesNotEmitAdminTransferred() public { + // AdminTransferred is reserved for the accept step. Recording logs proves it is not + // emitted on schedule. + vm.recordLogs(); vm.prank(INITIAL_ADMIN); registry.transferAdmin(newAdmin); + + bytes32 transferredSig = keccak256("AdminTransferred(address,address)"); + Vm.Log[] memory logs = vm.getRecordedLogs(); + for (uint256 i = 0; i < logs.length; i++) { + assertTrue(logs[i].topics[0] != transferredSig, "transfer must not emit AdminTransferred"); + } } function test_transferAdmin_revertsNotAdmin() public { @@ -87,26 +113,118 @@ contract SequencerRegistryTest is Test { registry.transferAdmin(newAdmin); } - function test_transferAdmin_revertsZeroAddress() public { + function test_transferAdmin_zeroCancelsPending() public { + vm.prank(INITIAL_ADMIN); + registry.transferAdmin(newAdmin); + assertEq(registry.pendingAdmin(), newAdmin); + + vm.expectEmit(true, true, false, false); + emit ISequencerRegistry.AdminTransferStarted(INITIAL_ADMIN, address(0)); + vm.prank(INITIAL_ADMIN); - vm.expectRevert(ISequencerRegistry.ZeroAddress.selector); registry.transferAdmin(address(0)); + assertEq(registry.pendingAdmin(), address(0)); + assertEq(registry.admin(), INITIAL_ADMIN, "current admin must not change on cancel"); } - function test_transferAdmin_newAdminCanActOldAdminCannot() public { + function test_transferAdmin_overwritesPending() public { vm.prank(INITIAL_ADMIN); registry.transferAdmin(newAdmin); - // Old admin cannot act + address otherCandidate = address(0x9999); + vm.prank(INITIAL_ADMIN); + registry.transferAdmin(otherCandidate); + assertEq(registry.pendingAdmin(), otherCandidate); + + // Original `newAdmin` must no longer be able to accept. + vm.prank(newAdmin); + vm.expectRevert(ISequencerRegistry.NotPendingAdmin.selector); + registry.acceptAdmin(); + } + + function test_transferAdmin_pendingDoesNotGrantAdminPowers() public { vm.prank(INITIAL_ADMIN); + registry.transferAdmin(newAdmin); + + // pendingAdmin cannot act as admin until they accept. + vm.prank(newAdmin); vm.expectRevert(ISequencerRegistry.NotAdmin.selector); - registry.transferAdmin(address(0x9999)); + registry.scheduleNextSystemAddressChange(address(0xDEAD), block.number + 1); + } + + // ============ acceptAdmin ============ + + function test_acceptAdmin_promotesPendingAndClearsSlot() public { + vm.prank(INITIAL_ADMIN); + registry.transferAdmin(newAdmin); + + vm.prank(newAdmin); + registry.acceptAdmin(); + + assertEq(registry.admin(), newAdmin); + assertEq(registry.pendingAdmin(), address(0)); + } + + function test_acceptAdmin_emitsAdminTransferred() public { + vm.prank(INITIAL_ADMIN); + registry.transferAdmin(newAdmin); + + vm.expectEmit(true, true, false, false); + emit ISequencerRegistry.AdminTransferred(INITIAL_ADMIN, newAdmin); + + vm.prank(newAdmin); + registry.acceptAdmin(); + } + + function test_acceptAdmin_revertsNotPendingAdmin() public { + vm.prank(INITIAL_ADMIN); + registry.transferAdmin(newAdmin); + + vm.prank(nonAdmin); + vm.expectRevert(ISequencerRegistry.NotPendingAdmin.selector); + registry.acceptAdmin(); + } + + function test_acceptAdmin_revertsWhenNoPending() public { + // No transfer has been started — pending is the default zero address. Even the old admin + // must be rejected because msg.sender != address(0). + vm.prank(INITIAL_ADMIN); + vm.expectRevert(ISequencerRegistry.NotPendingAdmin.selector); + registry.acceptAdmin(); + } + + function test_acceptAdmin_oldAdminCannotActAfterAccept() public { + vm.prank(INITIAL_ADMIN); + registry.transferAdmin(newAdmin); + vm.prank(newAdmin); + registry.acceptAdmin(); + + vm.prank(INITIAL_ADMIN); + vm.expectRevert(ISequencerRegistry.NotAdmin.selector); + registry.transferAdmin(address(0x1234)); + } + + function test_acceptAdmin_isOneShot() public { + vm.prank(INITIAL_ADMIN); + registry.transferAdmin(newAdmin); + vm.prank(newAdmin); + registry.acceptAdmin(); + + // Pending is cleared; calling acceptAdmin again must revert. + vm.prank(newAdmin); + vm.expectRevert(ISequencerRegistry.NotPendingAdmin.selector); + registry.acceptAdmin(); + } + + function test_fullHandoff_newAdminCanActAfterAccept() public { + vm.prank(INITIAL_ADMIN); + registry.transferAdmin(newAdmin); + vm.prank(newAdmin); + registry.acceptAdmin(); - // New admin can act - address anotherAdmin = address(0x1234); + // New admin can perform admin-only operations. vm.prank(newAdmin); - registry.transferAdmin(anotherAdmin); - assertEq(registry.admin(), anotherAdmin); + registry.scheduleNextSystemAddressChange(address(0xCAFE), block.number + 1); } // ============ scheduleNextSystemAddressChange ============ diff --git a/docs/spec/system-contracts/sequencer-registry.md b/docs/spec/system-contracts/sequencer-registry.md index 6e138c1..8b19677 100644 --- a/docs/spec/system-contracts/sequencer-registry.md +++ b/docs/spec/system-contracts/sequencer-registry.md @@ -36,7 +36,7 @@ A node MUST deploy the bytecode version corresponding to the active spec. Since: [Rex5](../upgrades/rex5.md) -Code hash: `0x2dd91bc339d4dadc8cec5a7096213af7cacb02bbbd97308e168564ee5357fb65` +Code hash: `0x63cd411a379be1c198613ef1d15c3058e7b0db4a5d07d4bcf07014af90040315` The contract is deployed via raw state patch with initial storage seeded at deploy time. No constructor is executed. @@ -51,41 +51,65 @@ Rust slot constants in `mega-system-contracts` must match this layout. | 0 | `_currentSystemAddress` | `address` | | 1 | `_currentSequencer` | `address` | | 2 | `_admin` | `address` | -| 3 | `_initialSystemAddress` | `address` | -| 4 | `_initialSequencer` | `address` | -| 5 | `_initialFromBlock` | `uint256` | -| 6 | `_pendingSystemAddress` | `address` | -| 7 | `_systemAddressActivationBlock` | `uint256` | -| 8 | `_pendingSequencer` | `address` | -| 9 | `_sequencerActivationBlock` | `uint256` | -| 10 | `_systemAddressHistory` | `ChangeRecord[]` | -| 11 | `_sequencerHistory` | `ChangeRecord[]` | +| 3 | `_pendingAdmin` | `address` | +| 4 | `_initialSystemAddress` | `address` | +| 5 | `_initialSequencer` | `address` | +| 6 | `_initialFromBlock` | `uint256` | +| 7 | `_pendingSystemAddress` | `address` | +| 8 | `_systemAddressActivationBlock` | `uint256` | +| 9 | `_pendingSequencer` | `address` | +| 10 | `_sequencerActivationBlock` | `uint256` | +| 11 | `_systemAddressHistory` | `ChangeRecord[]` | +| 12 | `_sequencerHistory` | `ChangeRecord[]` | `ChangeRecord` is packed: `uint96 fromBlock` + `address addr` fit in one 32-byte slot. +Future versions of `SequencerRegistry` may only **append** new slots; reordering or inserting in the middle is forbidden once the contract is in use, because dynamic-array element keys are derived from `keccak256(slot)` and any slot change orphans the existing data. + ### Interface ```solidity interface ISequencerRegistry { + // Packed history entry + struct ChangeRecord { + uint96 fromBlock; + address addr; + } + // System address role function currentSystemAddress() external view returns (address); function systemAddressAt(uint256 blockNumber) external view returns (address); function scheduleNextSystemAddressChange(address newSystemAddress, uint256 activationBlock) external; + event SystemAddressChangeScheduled( + address indexed oldSystemAddress, + address indexed newSystemAddress, + uint256 activationBlock + ); // Sequencer role function currentSequencer() external view returns (address); function sequencerAt(uint256 blockNumber) external view returns (address); function scheduleNextSequencerChange(address newSequencer, uint256 activationBlock) external; + event SequencerChangeScheduled( + address indexed oldSequencer, + address indexed newSequencer, + uint256 activationBlock + ); // Shared function applyPendingChanges() external; function admin() external view returns (address); - function transferAdmin(address newAdmin) external; + function pendingAdmin() external view returns (address); + function transferAdmin(address newAdmin) external; // step 1: schedule + function acceptAdmin() external; // step 2: complete + event AdminTransferStarted(address indexed currentAdmin, address indexed newPendingAdmin); + event AdminTransferred(address indexed oldAdmin, address indexed newAdmin); // Errors error FutureBlock(); error BeforeInitialBlock(); error NotAdmin(); + error NotPendingAdmin(); error ZeroAddress(); error InvalidActivationBlock(); error ActivationBlockTooLarge(); @@ -119,6 +143,23 @@ For each role, if pending and due, it updates the current address, appends to th The system call is issued with `gas_limit = max(block.gas_limit, 30_000_000)` instead of the upstream-fixed 30M used for EIP-2935 / EIP-4788. This is required because the slot-rotation cost scales with REX dynamic storage gas (SALT bucket capacity), and a fixed 30M is no longer guaranteed to be sufficient on activation blocks. +### Two-Step Admin Transfer + +Admin handoff is a two-step process to prevent permanent loss of admin authority through a single mistyped, phished, or clipboard-substituted address. + +1. The current admin calls `transferAdmin(newAdmin)`. + This sets `_pendingAdmin = newAdmin` and emits `AdminTransferStarted(currentAdmin, newAdmin)`. + The current admin remains in effect — `admin()` and all admin-only operations are unaffected until step 2. + Passing `address(0)` cancels any previously pending transfer. + Re-calling `transferAdmin` overwrites `_pendingAdmin`. +2. The pending admin calls `acceptAdmin()`. + This is the only way `_admin` is ever updated. + It sets `_admin = msg.sender`, clears `_pendingAdmin`, and emits `AdminTransferred(oldAdmin, newAdmin)`. + Any caller other than the current `_pendingAdmin` reverts with `NotPendingAdmin`. + +The acceptance step proves the new admin's keys are live and controlled. +Until acceptance, the old admin retains full authority and can re-target or cancel the pending transfer. + ### Interception `SequencerRegistry` does NOT use call interception. @@ -128,13 +169,20 @@ All methods run as normal on-chain bytecode. At first deploy, the execution layer writes 6 flat storage slots: `_currentSystemAddress`, `_currentSequencer`, `_admin`, `_initialSystemAddress`, `_initialSequencer`, `_initialFromBlock`. -The values come from `SequencerRegistryConfig` on the chain's hardfork configuration. +The sequencer and admin addresses come from `SequencerRegistryConfig` on the chain's hardfork configuration. + +**The initial system address is fixed.** +Both `_currentSystemAddress` and `_initialSystemAddress` are seeded with the legacy `MEGA_SYSTEM_ADDRESS` constant. +The genesis value is not configurable on `SequencerRegistryConfig`. +Pre-Rex5 components (payload executor, txpool, replay) all assume the system tx sender equals `MEGA_SYSTEM_ADDRESS`, so a configurable initial value would silently break those invariants at chain bootstrap. +After Rex5 activation, the system address can be rotated via `scheduleNextSystemAddressChange` + `applyPendingChanges`. + +**Validation constraints:** Both configurable address fields — `rex5_initial_sequencer` and `rex5_initial_admin` — must be non-zero. +The chain configuration MUST reject either zero address before the first block using this registry is executed. +A zero `rex5_initial_admin` would permanently lock all admin-only registry operations. +A zero `rex5_initial_sequencer` produces an invalid initial sequencer state. -**Validation constraints:** All three address fields — `initial_system_address`, `initial_sequencer`, and `initial_admin` — must be non-zero. -Deployment fails with a block execution error if any of them is the zero address. -A zero `initial_admin` would permanently lock all admin-only registry operations. -A zero `initial_system_address` would break system-address resolution after deployment. -A zero `initial_sequencer` produces an invalid initial sequencer state. +The `rex5_` prefix on these field names is deliberate: the values only take effect when Rex5 activates and seed the registry at that moment; pre-Rex5 blocks ignore them entirely. ## Constants diff --git a/docs/spec/upgrades/rex5.md b/docs/spec/upgrades/rex5.md index 2015eb3..df1e4fe 100644 --- a/docs/spec/upgrades/rex5.md +++ b/docs/spec/upgrades/rex5.md @@ -32,9 +32,10 @@ Key methods: - `systemAddressAt(blockNumber)` / `sequencerAt(blockNumber)` — historical role lookups. - `scheduleNextSystemAddressChange(...)` / `scheduleNextSequencerChange(...)` — admin schedules a change for either role. - `applyPendingChanges()` — permissionless; applies both roles atomically as a pre-block system call. -- `admin()` / `transferAdmin(newAdmin)` — admin management. +- `admin()` / `pendingAdmin()` / `transferAdmin(newAdmin)` / `acceptAdmin()` — two-step admin handoff. `transferAdmin` only sets `_pendingAdmin`; the new admin must call `acceptAdmin` for the change to take effect, preventing single-step lockouts from a mistyped or phished address. -Initial storage is seeded at deploy time from the chain's `SequencerRegistryConfig`. +Initial storage is seeded at deploy time. +The initial system address is fixed to `MEGA_SYSTEM_ADDRESS` and is not configurable on `SequencerRegistryConfig`; the initial sequencer and admin come from the chain's `SequencerRegistryConfig`. No constructor is executed. ### 2. Dynamic System Address