#[cfg(not(feature = "std"))]
use alloc as std;
use alloy_evm::Database;
use alloy_primitives::{keccak256, Address, Bytes, B256, U256};
use revm::{
database::State,
state::{Account, Bytecode, EvmState, EvmStorageSlot},
};
use std::vec::Vec;
use crate::MegaHardforks;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SystemContractSpec {
pub address: Address,
pub code: Bytes,
pub code_hash: B256,
pub seed: Vec<(U256, U256)>,
pub force_create_on_upgrade: bool,
}
impl SystemContractSpec {
pub fn new(address: Address, code: Bytes, code_hash: B256) -> Self {
Self { address, code, code_hash, seed: Vec::new(), force_create_on_upgrade: false }
}
pub fn with_seed(mut self, seed: Vec<(U256, U256)>) -> Self {
self.seed = seed;
self
}
pub fn with_force_create_on_upgrade(mut self, force: bool) -> Self {
self.force_create_on_upgrade = force;
self
}
}
pub fn transact_deploy<DB: Database>(
db: &mut State<DB>,
spec: &SystemContractSpec,
) -> Result<EvmState, DB::Error> {
debug_assert_eq!(
keccak256(spec.code.as_ref()),
spec.code_hash,
"SystemContractSpec code_hash does not match code for {:?}",
spec.address
);
let acc = db.load_cache_account(spec.address)?;
let existing_info = acc.account_info();
if let Some(account_info) = &existing_info {
if account_info.code_hash == spec.code_hash {
return Ok(EvmState::from_iter([(
spec.address,
Account { info: account_info.clone(), ..Default::default() },
)]));
}
}
let account_existed = existing_info.is_some();
let mut acc_info = existing_info.unwrap_or_default();
acc_info.code_hash = spec.code_hash;
acc_info.code = Some(Bytecode::new_raw(spec.code.clone()));
let mut revm_acc: Account = acc_info.into();
revm_acc.mark_touch();
if !account_existed || spec.force_create_on_upgrade {
revm_acc.mark_created();
for (slot, value) in &spec.seed {
revm_acc.storage.insert(*slot, EvmStorageSlot::new_changed(U256::ZERO, *value, 0));
}
}
Ok(EvmState::from_iter([(spec.address, revm_acc)]))
}
pub fn flat_system_contract_specs(
hardforks: impl MegaHardforks,
block_timestamp: u64,
) -> Vec<SystemContractSpec> {
[
super::oracle::oracle_spec(&hardforks, block_timestamp),
super::oracle::high_precision_timestamp_oracle_spec(&hardforks, block_timestamp),
super::keyless_deploy::keyless_deploy_spec(&hardforks, block_timestamp),
super::control::access_control_spec(&hardforks, block_timestamp),
super::limit_control::limit_control_spec(&hardforks, block_timestamp),
]
.into_iter()
.flatten()
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
MegaHardfork, MegaHardforkConfig, ORACLE_CONTRACT_ADDRESS, ORACLE_CONTRACT_CODE_HASH,
ORACLE_CONTRACT_CODE_HASH_REX2, ORACLE_CONTRACT_CODE_HASH_REX5,
};
use alloy_hardforks::ForkCondition;
use alloy_primitives::address;
use revm::{database::InMemoryDB, state::AccountInfo, Database as _, DatabaseCommit};
fn addrs(specs: &[SystemContractSpec]) -> Vec<Address> {
specs.iter().map(|s| s.address).collect()
}
const SEEDED_ADDR: Address = address!("0x6342000000000000000000000000000000000099");
const SEED_SLOT: U256 = U256::from_limbs([7, 0, 0, 0]);
const UNRELATED_SLOT: U256 = U256::from_limbs([5, 0, 0, 0]);
fn seeded_spec(force_create_on_upgrade: bool) -> SystemContractSpec {
let code = Bytes::from_static(&[0x60, 0x00]);
let code_hash = keccak256(&code);
SystemContractSpec::new(SEEDED_ADDR, code, code_hash)
.with_seed(Vec::from([(SEED_SLOT, U256::from(42))]))
.with_force_create_on_upgrade(force_create_on_upgrade)
}
#[test]
fn test_seed_skipped_on_storage_preserving_upgrade() {
let mut db = InMemoryDB::default();
let existing_code = Bytes::from_static(&[0xfe]);
db.insert_account_info(
SEEDED_ADDR,
AccountInfo {
balance: U256::from(1),
nonce: 1,
code_hash: keccak256(&existing_code),
code: Some(Bytecode::new_raw(existing_code)),
},
);
db.insert_account_storage(SEEDED_ADDR, SEED_SLOT, U256::from(99)).unwrap();
let mut state = State::builder().with_database(&mut db).build();
let result = transact_deploy(&mut state, &seeded_spec(false)).unwrap();
let account = result.get(&SEEDED_ADDR).unwrap();
assert!(account.is_touched());
assert!(!account.is_created(), "storage-preserving upgrade must not recreate");
assert!(
!account.storage.contains_key(&SEED_SLOT),
"seed must not be written when storage is preserved"
);
state.commit(result);
assert_eq!(
state.storage(SEEDED_ADDR, SEED_SLOT).unwrap(),
U256::from(99),
"preserved storage must keep its pre-upgrade value after commit"
);
}
#[test]
fn test_seed_written_on_force_created_upgrade() {
let mut db = InMemoryDB::default();
let existing_code = Bytes::from_static(&[0xfe]);
db.insert_account_info(
SEEDED_ADDR,
AccountInfo {
balance: U256::from(1),
nonce: 1,
code_hash: keccak256(&existing_code),
code: Some(Bytecode::new_raw(existing_code)),
},
);
db.insert_account_storage(SEEDED_ADDR, UNRELATED_SLOT, U256::from(99)).unwrap();
let mut state = State::builder().with_database(&mut db).build();
let result = transact_deploy(&mut state, &seeded_spec(true)).unwrap();
let account = result.get(&SEEDED_ADDR).unwrap();
assert!(account.is_created(), "force_create_on_upgrade must recreate");
let slot = account.storage.get(&SEED_SLOT).expect("seed slot must be written");
assert_eq!(slot.present_value, U256::from(42));
assert_eq!(slot.original_value, U256::ZERO);
state.commit(result);
assert_eq!(state.storage(SEEDED_ADDR, SEED_SLOT).unwrap(), U256::from(42));
assert_eq!(
state.storage(SEEDED_ADDR, UNRELATED_SLOT).unwrap(),
U256::ZERO,
"recreation must clear unrelated pre-existing storage"
);
}
#[test]
fn test_seed_written_on_fresh_deploy() {
let mut db = InMemoryDB::default();
let mut state = State::builder().with_database(&mut db).build();
let result = transact_deploy(&mut state, &seeded_spec(false)).unwrap();
let account = result.get(&SEEDED_ADDR).unwrap();
assert!(account.is_created(), "fresh deploy creates the account");
assert_eq!(
account.storage.get(&SEED_SLOT).expect("seed slot must be written").present_value,
U256::from(42)
);
}
#[test]
fn test_registry_empty_before_mini_rex() {
let hf = MegaHardforkConfig::default()
.with(MegaHardfork::MiniRex, ForkCondition::Timestamp(100));
assert!(flat_system_contract_specs(&hf, 99).is_empty());
}
#[test]
fn test_registry_grows_with_hardforks_and_picks_oracle_version() {
let hf = MegaHardforkConfig::default().with_all_activated();
let specs = flat_system_contract_specs(&hf, 0);
assert_eq!(specs.len(), 5);
assert_eq!(specs[0].address, ORACLE_CONTRACT_ADDRESS);
assert_eq!(specs[0].code_hash, ORACLE_CONTRACT_CODE_HASH_REX5);
assert!(!specs[0].force_create_on_upgrade);
assert!(!addrs(&specs).contains(&crate::SEQUENCER_REGISTRY_ADDRESS));
}
#[test]
fn test_registry_oracle_version_by_spec() {
let mini =
MegaHardforkConfig::default().with(MegaHardfork::MiniRex, ForkCondition::Timestamp(0));
let mini = flat_system_contract_specs(&mini, 0);
assert_eq!(mini.len(), 2);
assert_eq!(mini[0].code_hash, ORACLE_CONTRACT_CODE_HASH);
assert!(mini[0].force_create_on_upgrade);
let rex2 = MegaHardforkConfig::default()
.with(MegaHardfork::MiniRex, ForkCondition::Timestamp(0))
.with(MegaHardfork::Rex2, ForkCondition::Timestamp(0));
let rex2 = flat_system_contract_specs(&rex2, 0);
assert_eq!(rex2[0].code_hash, ORACLE_CONTRACT_CODE_HASH_REX2);
assert!(rex2[0].force_create_on_upgrade);
}
}