Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions crates/mega-evm/src/evm/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,13 @@ impl<DB: revm::Database> JournalInspectTr for Journal<DB> {
) -> 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).
Expand All @@ -457,9 +464,15 @@ impl<DB: revm::Database> JournalInspectTr for Journal<DB> {
};
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);
Comment thread
flyq marked this conversation as resolved.
Comment thread
flyq marked this conversation as resolved.
// 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
Expand Down
41 changes: 41 additions & 0 deletions crates/mega-evm/tests/mini_rex/db_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading