diff --git a/Cargo.lock b/Cargo.lock index 89c67299..d5e814a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2922,7 +2922,7 @@ dependencies = [ [[package]] name = "mega-evm" -version = "1.5.1" +version = "1.5.2" dependencies = [ "alloy-consensus", "alloy-eips", @@ -2956,7 +2956,7 @@ dependencies = [ [[package]] name = "mega-evme" -version = "1.5.1" +version = "1.5.2" dependencies = [ "alloy-consensus", "alloy-eips", @@ -2986,7 +2986,7 @@ dependencies = [ [[package]] name = "mega-system-contracts" -version = "1.5.1" +version = "1.5.2" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -4811,7 +4811,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "state-test" -version = "1.5.1" +version = "1.5.2" dependencies = [ "alloy-eips", "alloy-primitives", diff --git a/Cargo.toml b/Cargo.toml index ed23db4d..8d550160 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ exclude = [] # https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html resolver = "2" [workspace.package] -version = "1.5.1" +version = "1.5.2" edition = "2021" rust-version = "1.86" license = "MIT OR Apache-2.0" diff --git a/crates/mega-evm/src/evm/host.rs b/crates/mega-evm/src/evm/host.rs index 82a8ee4e..10f867f7 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 10230fdd..cd54ac33 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