Skip to content

Commit 7c32692

Browse files
authored
Fix contract storage migration (#500)
1 parent 6588053 commit 7c32692

File tree

6 files changed

+125
-22
lines changed

6 files changed

+125
-22
lines changed

crates/evm/core/src/backend/cow.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,10 @@ impl DatabaseExt for CowBackend<'_> {
291291
self.backend.cached_accounts()
292292
}
293293

294+
fn cached_storage(&self, address: Address) -> Option<Map<U256, U256>> {
295+
self.backend.cached_storage(address)
296+
}
297+
294298
fn set_blockhash(&mut self, block_number: U256, block_hash: B256) {
295299
self.backend.to_mut().set_blockhash(block_number, block_hash);
296300
}

crates/evm/core/src/backend/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,11 @@ pub trait DatabaseExt: Database<Error = DatabaseError> + DatabaseCommit + Debug
356356
/// Returns all accounts in the memory database cache
357357
fn cached_accounts(&self) -> Vec<Address>;
358358

359+
/// Returns the cached storage for an account from the memory database cache.
360+
/// This returns storage that was written during contract construction before
361+
/// startup migration, which may not yet be in the journaled state.
362+
fn cached_storage(&self, address: Address) -> Option<Map<U256, U256>>;
363+
359364
/// Ensures that `account` is allowed to execute cheatcodes
360365
///
361366
/// Returns an error if [`Self::has_cheatcode_access`] returns `false`
@@ -1535,6 +1540,14 @@ impl DatabaseExt for Backend {
15351540
.collect()
15361541
}
15371542

1543+
fn cached_storage(&self, address: Address) -> Option<Map<U256, U256>> {
1544+
self.mem_db
1545+
.cache
1546+
.accounts
1547+
.get(&address)
1548+
.map(|acc| acc.storage.iter().map(|(k, v)| (*k, *v)).collect())
1549+
}
1550+
15381551
fn set_blockhash(&mut self, block_number: U256, block_hash: B256) {
15391552
if let Some(db) = self.active_fork_db_mut() {
15401553
db.cache.block_hashes.insert(block_number.saturating_to(), block_hash);

crates/forge/tests/it/revive/migration.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,14 @@ async fn test_initial_contract_deployment(#[case] runtime_mode: ReviveRuntimeMod
176176
Filter::new("testInitialContractMigration", "InitialMigrationTest", ".*/revive/.*");
177177
TestConfig::with_filter(runner, filter).spec_id(SpecId::PRAGUE).run().await;
178178
}
179+
180+
#[rstest]
181+
#[case::pvm(ReviveRuntimeMode::Pvm)]
182+
#[case::evm(ReviveRuntimeMode::Evm)]
183+
#[tokio::test(flavor = "multi_thread")]
184+
async fn test_contract_with_constructor_deployment(#[case] runtime_mode: ReviveRuntimeMode) {
185+
let runner = TEST_DATA_REVIVE.runner_revive(runtime_mode);
186+
let filter =
187+
Filter::new("testConstructorArgPreserved", "ContractArgsMigrationTest", ".*/revive/.*");
188+
TestConfig::with_filter(runner, filter).spec_id(SpecId::PRAGUE).run().await;
189+
}

crates/revive-strategy/src/cheatcodes/mod.rs

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -354,15 +354,15 @@ impl CheatcodeInspectorStrategyRunner for PvmCheatcodeInspectorStrategyRunner {
354354
}
355355
t if using_revive && is::<rollCall>(t) => {
356356
let &rollCall { newHeight } = cheatcode.as_any().downcast_ref().unwrap();
357-
let new_block_number: u64 = newHeight.try_into().expect("Block number exceeds u32");
357+
let new_block_number: u64 = newHeight.saturating_to();
358358

359359
// blockhash should be the same on both revive and revm sides, so fetch it before
360360
// changing the block number.
361361
let prev_new_height_hash = ccx
362362
.ecx
363363
.journaled_state
364364
.database
365-
.block_hash(new_block_number - 1)
365+
.block_hash(new_block_number.saturating_sub(1))
366366
.expect("Should not fail");
367367
let new_height_hash = ccx
368368
.ecx
@@ -695,10 +695,11 @@ fn select_revive(
695695
}
696696

697697
for address in accounts {
698-
tracing::info!("Migrating account {:?} (is_test_contract: {})", address, test_contract_addr == Some(address));
698+
tracing::info!("Migrating account {:?} (is_test_contract: {})", address, test_contract_addr == Some(address));
699699
let acc = data.journaled_state.load_account(address).expect("failed to load account");
700-
let amount = acc.data.info.balance;
701-
let nonce = acc.data.info.nonce;
700+
701+
let amount = acc.info.balance;
702+
let nonce = acc.info.nonce;
702703
let account = H160::from_slice(address.as_slice());
703704
let account_id =
704705
AccountId32Mapper::<Runtime>::to_fallback_account_id(&account);
@@ -710,7 +711,7 @@ fn select_revive(
710711
a.nonce = nonce.min(u32::MAX.into()).try_into().expect("shouldn't happen");
711712
});
712713

713-
if let Some(bytecode) = acc.data.info.code.as_ref() {
714+
if let Some(bytecode) = acc.info.code.as_ref() {
714715
let account_h160 = H160::from_slice(address.as_slice());
715716

716717
// Skip if contract already exists in pallet-revive
@@ -825,26 +826,73 @@ fn select_revive(
825826
}
826827
}
827828
}
828-
if AccountInfo::<Runtime>::load_contract(&account_h160).is_some() {
829-
// Migrate complete account state (storage) for newly created/existing contract
830-
for (slot, storage_slot) in &acc.data.storage {
831-
let slot_bytes = slot.to_be_bytes::<32>();
832-
let value_bytes = storage_slot.present_value.to_be_bytes::<32>();
833-
834-
if !storage_slot.present_value.is_zero() {
835-
let _ = Pallet::<Runtime>::set_storage(
836-
account_h160,
837-
slot_bytes,
838-
Some(value_bytes.to_vec()),
839-
);
840-
}
841-
}
829+
if AccountInfo::<Runtime>::load_contract(&account_h160).is_some() {
830+
migrate_contract_storage(data, address, account_h160);
842831
}
843832
}
844833
}
845834
});
846835
}
847836

837+
/// Migrates contract storage from REVM state to pallet-revive.
838+
///
839+
/// Merges storage from two sources:
840+
/// 1. Journaled state: most recent storage values from current execution
841+
/// 2. Database cache: storage written before startup migration, which is run as separate
842+
/// transaction, already committed to cache
843+
///
844+
/// The journaled state takes precedence - cache values are only used for slots
845+
/// not present in the journaled state.
846+
fn migrate_contract_storage(data: Ecx<'_, '_, '_>, address: Address, account_h160: H160) {
847+
use std::collections::HashSet;
848+
849+
// Track which slots we've already migrated from the journaled state
850+
let mut migrated_slots: HashSet<U256> = HashSet::new();
851+
852+
// First, migrate storage from the journaled state (most up-to-date values)
853+
if let Some(account_state) = data.journaled_state.state.get(&address) {
854+
for (slot, storage_slot) in &account_state.storage {
855+
migrated_slots.insert(*slot);
856+
857+
let slot_bytes = slot.to_be_bytes::<32>();
858+
let value = storage_slot.present_value;
859+
860+
if !value.is_zero() {
861+
let _ = Pallet::<Runtime>::set_storage(
862+
account_h160,
863+
slot_bytes,
864+
Some(value.to_be_bytes::<32>().to_vec()),
865+
);
866+
} else {
867+
// Handle case where storage was explicitly cleared
868+
let _ = Pallet::<Runtime>::set_storage(account_h160, slot_bytes, None);
869+
}
870+
}
871+
}
872+
873+
// Then, migrate storage from the database cache for slots NOT in journaled state
874+
if let Some(cached_storage) = data.journaled_state.database.cached_storage(address) {
875+
for (slot, value) in cached_storage {
876+
// Skip slots already migrated from the journaled state
877+
if migrated_slots.contains(&slot) {
878+
continue;
879+
}
880+
881+
let slot_bytes = slot.to_be_bytes::<32>();
882+
if !value.is_zero() {
883+
let _ = Pallet::<Runtime>::set_storage(
884+
account_h160,
885+
slot_bytes,
886+
Some(value.to_be_bytes::<32>().to_vec()),
887+
);
888+
} else {
889+
// Handle case where storage was explicitly cleared
890+
let _ = Pallet::<Runtime>::set_storage(account_h160, slot_bytes, None);
891+
}
892+
}
893+
}
894+
}
895+
848896
fn select_evm(ctx: &mut PvmCheatcodeInspectorStrategyContext, data: Ecx<'_, '_, '_>) {
849897
if !ctx.using_revive {
850898
tracing::info!("already using REVM");
@@ -991,7 +1039,7 @@ impl foundry_cheatcodes::CheatcodeInspectorStrategyExt for PvmCheatcodeInspector
9911039
(contract.resolc_bytecode.as_bytes().unwrap().to_vec(), constructor_args.to_vec())
9921040
}
9931041
crate::ReviveRuntimeMode::Evm => {
994-
// EVM mode: use EVM bytecode directly
1042+
// EVM mode: use EVM bytecode directly (includes constructor args)
9951043
tracing::info!("running create in EVM mode with EVM bytecode");
9961044
(init_code.0.to_vec(), vec![])
9971045
}

crates/revive-strategy/src/state.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ impl TestEnv {
115115
) {
116116
// Set block number in pallet-revive runtime.
117117
self.0.lock().unwrap().externalities.execute_with(|| {
118-
let new_block_number: u64 = new_height.try_into().expect("Block number exceeds u64");
118+
let new_block_number: u64 = new_height.saturating_to();
119119
let digest = System::digest();
120120
if System::block_hash(new_block_number) == H256::zero() {
121121
// First initialize and finalize the parent block to set up correct hashes.

testdata/default/revive/EvmToReviveMigration.t.sol

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,3 +384,30 @@ contract InitialMigrationTest is DSTest {
384384
assertEq(storageContract.get(), 42);
385385
}
386386
}
387+
388+
// Simple contract that stores a constructor argument
389+
contract SimpleAddrStorage {
390+
address public storedAddr;
391+
392+
constructor(address _addr) {
393+
storedAddr = _addr;
394+
}
395+
}
396+
397+
contract ContractArgsMigrationTest is DSTest {
398+
Vm constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));
399+
address myAddr = vm.addr(1900);
400+
401+
// Expected address for private key 1900
402+
address constant EXPECTED = 0x6Af741AA4Ff39CF3De9A0Cb02A8Bab387E41abFB;
403+
SimpleAddrStorage store = new SimpleAddrStorage(myAddr);
404+
405+
function testConstructorArgPreserved() public {
406+
emit log_named_address("myAddr (from vm.addr)", myAddr);
407+
emit log_named_address("store.storedAddr()", store.storedAddr());
408+
emit log_named_address("EXPECTED", EXPECTED);
409+
410+
assertEq(myAddr, EXPECTED, "vm.addr(1900) should return correct address");
411+
assertEq(store.storedAddr(), myAddr, "Constructor arg should be preserved");
412+
}
413+
}

0 commit comments

Comments
 (0)