diff --git a/crates/mega-evm/src/evm/host.rs b/crates/mega-evm/src/evm/host.rs index f45c3c0..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). @@ -457,9 +464,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 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