From 12d625083e01ca4fc5b6f78e39541e8ffe7fd147 Mon Sep 17 00:00:00 2001 From: "liquan.eth" Date: Wed, 6 May 2026 22:15:51 +0800 Subject: [PATCH 1/3] fix(evm): skip DB storage lookup for newly-created accounts in inspect_storage --- crates/mega-evm/src/evm/host.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/mega-evm/src/evm/host.rs b/crates/mega-evm/src/evm/host.rs index f45c3c0..ab17949 100644 --- a/crates/mega-evm/src/evm/host.rs +++ b/crates/mega-evm/src/evm/host.rs @@ -447,6 +447,9 @@ impl JournalInspectTr for Journal { } else { self.inspect_account_delegated(spec, address)? }; + // Capture before any potential re-borrow. Used below to skip the DB lookup for + // accounts created in this transaction, mirroring revm's `sload_with_account`. + let is_newly_created = account.is_created(); if account.storage.contains_key(&key) { // Slot already exists, return reference to it. // Need to reload account to satisfy borrow checker. @@ -457,9 +460,15 @@ impl JournalInspectTr for Journal { }; return Ok(account.storage.get(&key).unwrap()); } - // Slot doesn't exist, load from DB and insert - let slot = self.database.storage(address, key)?; - let mut slot = EvmStorageSlot::new(slot, transaction_id); + // Slot doesn't exist. For newly-created accounts, post-CREATE storage is + // guaranteed empty (EIP-161 / EIP-6780), so return ZERO without touching the DB. + // Querying here would otherwise generate a witness lookup for a slot that has no + // meaningful pre-state value — which fails for stateless replay when CREATE lands + // on a pre-funded address (its `Loaded` cache status bypasses revm's + // `State::storage` short-circuit and exposes the call to the witness backend). + let slot_value = + if is_newly_created { U256::ZERO } else { self.database.storage(address, key)? }; + let mut slot = EvmStorageSlot::new(slot_value, transaction_id); // deliberately mark the slot as cold since we are only inspecting it, not warming it slot.mark_cold(); // Load account again to bypass the borrow checker and insert the slot From bbada0d69c8f144c21e18a5ef237373dac3f98b0 Mon Sep 17 00:00:00 2001 From: "liquan.eth" Date: Wed, 6 May 2026 23:01:37 +0800 Subject: [PATCH 2/3] add test case --- crates/mega-evm/tests/mini_rex/db_error.rs | 41 ++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/crates/mega-evm/tests/mini_rex/db_error.rs b/crates/mega-evm/tests/mini_rex/db_error.rs index 10230fd..cd54ac3 100644 --- a/crates/mega-evm/tests/mini_rex/db_error.rs +++ b/crates/mega-evm/tests/mini_rex/db_error.rs @@ -135,6 +135,47 @@ fn test_call_with_transfer_db_error_on_inspect_account() { } } +/// Regression: `inspect_storage` must not query `DB::storage` for accounts created in the +/// current transaction. Their post-CREATE storage is guaranteed empty (EIP-161 / EIP-6780), +/// so any DB read returns ZERO at best — and fails outright under stateless replay, where +/// no witness exists for these slots when CREATE lands on a pre-funded address (its +/// `Loaded` cache status bypasses revm's `State::storage` short-circuit). +/// +/// The injected DB is configured to error on every `storage()` read of slot 0 at the +/// to-be-created address. With the fix, `inspect_storage` short-circuits to ZERO for +/// newly-created accounts and the error is never triggered, so the CREATE succeeds. +/// Without the fix, the constructor's SSTORE pre-read would surface `EVMError::Custom` +/// (mini-rex routes SSTORE through `additional_limit_ext::sstore`, which calls +/// `inspect_storage` to compute the original/present values before writing). +#[test] +fn test_inspect_storage_skips_db_for_newly_created_account() { + // Initcode: SSTORE slot 0 = 0x42; STOP. The SSTORE pre-read goes through + // `inspect_storage` and is the path the fix targets. + let initcode = BytecodeBuilder::default().sstore(U256::ZERO, U256::from(0x42)).stop().build(); + + // CALLER's nonce starts at 0, so the top-level CREATE deploys to `CALLER.create(0)`. + let created = CALLER.create(0); + + let mut inner_db = MemoryDatabase::default(); + inner_db.set_account_balance(CALLER, U256::from(100_000_000_000u64)); + // Pre-fund the future contract address so its DB cache status is `Loaded` rather than + // `Vacant` — this is the scenario that exposed the original bug. + inner_db.set_account_balance(created, U256::from(1)); + + let mut db = ErrorInjectingDatabase::new(inner_db); + // The fix means this DB call must never happen. If it does, the test fails. + db.fail_on_storage = Some((created, U256::ZERO)); + + let result = transact(MegaSpecId::MINI_REX, db, CALLER, None, initcode, U256::ZERO, 10_000_000); + + let res = result.expect("CREATE should not surface a DB error"); + assert!( + res.result.is_success(), + "CREATE should succeed without DB::storage being queried, got: {:?}", + res.result + ); +} + /// When `inspect_account_delegated` fails during STATICCALL (in `wrap_call_with_storage_gas!`), /// the EVM should halt with `FatalExternalError`. /// This tests a different code path from CALL-with-transfer: STATICCALL has no value parameter From 47c7e3b1690e28a1c195db18129f66d91f508865 Mon Sep 17 00:00:00 2001 From: "liquan.eth" Date: Thu, 7 May 2026 11:35:42 +0800 Subject: [PATCH 3/3] Update host.rs --- crates/mega-evm/src/evm/host.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/mega-evm/src/evm/host.rs b/crates/mega-evm/src/evm/host.rs index ab17949..1422623 100644 --- a/crates/mega-evm/src/evm/host.rs +++ b/crates/mega-evm/src/evm/host.rs @@ -439,6 +439,13 @@ impl JournalInspectTr for Journal { ) -> Result<&EvmStorageSlot, Self::DBError> { let transaction_id = self.transaction_id; let is_rex4_enabled = spec.is_enabled(MegaSpecId::REX4); + // EIP-7702 storage semantics: storage belongs to the original address (delegator), + // not the delegate. So `is_created` must be checked on the original address — an + // EOA delegating via 7702 is never CREATEd, so its flag is always false. Checking + // the delegate's flag instead would mistakenly short-circuit storage reads when the + // delegate happens to be a freshly-CREATEd contract in the same tx, corrupting + // SSTORE accounting (gas / kv_updates / data_size) on the delegator's slots. + let is_newly_created = inspect_account(self, address)?.is_created(); // REX4+: storage belongs to the original address, not the delegate — do not follow // EIP-7702 delegation here (matching upstream revm's sload behavior). // Pre-REX4: follows delegation (original behavior). @@ -447,9 +454,6 @@ impl JournalInspectTr for Journal { } else { self.inspect_account_delegated(spec, address)? }; - // Capture before any potential re-borrow. Used below to skip the DB lookup for - // accounts created in this transaction, mirroring revm's `sload_with_account`. - let is_newly_created = account.is_created(); if account.storage.contains_key(&key) { // Slot already exists, return reference to it. // Need to reload account to satisfy borrow checker.