diff --git a/crates/forge/tests/it/revive/cheat_etch.rs b/crates/forge/tests/it/revive/cheat_etch.rs index 87b043e58d63b..9853ebc2f3fb0 100644 --- a/crates/forge/tests/it/revive/cheat_etch.rs +++ b/crates/forge/tests/it/revive/cheat_etch.rs @@ -10,7 +10,7 @@ use rstest::rstest; #[tokio::test(flavor = "multi_thread")] async fn test_etch(#[case] runtime_mode: ReviveRuntimeMode) { let runner: forge::MultiContractRunner = TEST_DATA_REVIVE.runner_revive(runtime_mode); - let filter = Filter::new(".*", "EtchTest", ".*/revive/EtchTest.t.sol"); + let filter = Filter::new(".*", ".*", ".*/revive/EtchTest.t.sol"); TestConfig::with_filter(runner, filter).spec_id(SpecId::PRAGUE).run().await; } diff --git a/crates/revive-env/src/lib.rs b/crates/revive-env/src/lib.rs index deee6eb2805a9..5463b4f251949 100644 --- a/crates/revive-env/src/lib.rs +++ b/crates/revive-env/src/lib.rs @@ -18,7 +18,10 @@ use polkadot_sdk::{ sp_tracing, }; -pub use crate::runtime::{Balance, BlockAuthor, GasScale, Runtime, System, Timestamp}; +pub use crate::runtime::{ + Balance, Balances, BlockAuthor, GasScale, NativeToEthRatio, Runtime, RuntimeHoldReason, System, + Timestamp, +}; pub use polkadot_sdk::parachains_common::AccountId; mod runtime; diff --git a/crates/revive-strategy/src/state.rs b/crates/revive-strategy/src/state.rs index 4689eae1b5d98..eef0fef6bc71e 100644 --- a/crates/revive-strategy/src/state.rs +++ b/crates/revive-strategy/src/state.rs @@ -1,16 +1,21 @@ use alloy_primitives::{Address, B256, Bytes, FixedBytes, U256}; use foundry_cheatcodes::{Error, Result}; use polkadot_sdk::{ + frame_support::traits::{ + fungible::{InspectHold, MutateHold}, + tokens::Precision, + }, pallet_revive::{ self, AccountId32Mapper, AccountInfo, AddressMapper, BytecodeType, ContractInfo, - ExecConfig, Executable, Pallet, ResourceMeter, + ExecConfig, Executable, HoldReason, Pallet, ResourceMeter, }, sp_core::{self, H160, H256}, sp_externalities::Externalities, sp_io::TestExternalities, + sp_runtime::AccountId32, sp_weights::Weight, }; -use revive_env::{BlockAuthor, ExtBuilder, Runtime, System, Timestamp}; +use revive_env::{Balances, BlockAuthor, ExtBuilder, NativeToEthRatio, Runtime, System, Timestamp}; use std::{ fmt::Debug, sync::{Arc, Mutex}, @@ -188,6 +193,51 @@ impl TestEnv { }) } + fn set_base_deposit_hold( + target_address: &H160, + target_account: &AccountId32, + contract_info: &mut ContractInfo, + code_deposit: u128, + ) -> foundry_cheatcodes::Result { + contract_info.update_base_deposit(code_deposit); + + let base_deposit: u128 = contract_info.storage_base_deposit(); + let hold_reason: revive_env::RuntimeHoldReason = HoldReason::StorageDepositReserve.into(); + + // Release any existing hold + let current_held = Balances::balance_on_hold(&hold_reason, target_account); + if current_held > 0 { + Balances::release(&hold_reason, target_account, current_held, Precision::BestEffort) + .map_err(|_| <&str as Into>::into("Could not release old hold"))?; + + // Decrease EVM balance by released amount (hold became free, so visible balance would + // increase) + let current_evm_balance = Pallet::::evm_balance(target_address); + let release_wei = sp_core::U256::from(current_held) + .saturating_mul(sp_core::U256::from(NativeToEthRatio::get() as u128)); + let adjusted_balance = current_evm_balance.saturating_sub(release_wei); + Pallet::::set_evm_balance(target_address, adjusted_balance).map_err(|_| { + <&str as Into>::into("Could not adjust balance after release") + })?; + } + + // Create new hold with correct amount + if base_deposit > 0 { + let current_evm_balance = Pallet::::evm_balance(target_address); + let hold_wei = sp_core::U256::from(base_deposit) + .saturating_mul(sp_core::U256::from(NativeToEthRatio::get() as u128)); + let new_evm_balance = current_evm_balance.saturating_add(hold_wei); + + Pallet::::set_evm_balance(target_address, new_evm_balance) + .map_err(|_| <&str as Into>::into("Could not set balance for new hold"))?; + + Balances::hold(&hold_reason, target_account, base_deposit) + .map_err(|_| <&str as Into>::into("Could not create new hold"))?; + } + + Ok(Default::default()) + } + pub fn etch_call(&mut self, target: &Address, new_runtime_code: &Bytes) -> Result { self.0.lock().unwrap().externalities.execute_with(|| { let target_address = H160::from_slice(target.as_slice()); @@ -210,6 +260,9 @@ impl TestEnv { ) .map_err(|_| <&str as Into>::into("Could not upload PVM code"))?; + let code_deposit = contract_blob.code_info().deposit(); + let code_hash = *contract_blob.code_hash(); + let mut contract_info = if let Some(contract_info) = AccountInfo::::load_contract(&target_address) { @@ -217,8 +270,8 @@ impl TestEnv { } else { let contract_info = ContractInfo::::new( &target_address, - System::account_nonce(target_account), - *contract_blob.code_hash(), + System::account_nonce(&target_account), + code_hash, ) .map_err(|err| { tracing::error!("Could not create contract info: {:?}", err); @@ -229,11 +282,23 @@ impl TestEnv { )); contract_info }; - contract_info.code_hash = *contract_blob.code_hash(); + + contract_info.code_hash = code_hash; + + // Update base deposit hold for both new and existing contracts + // Note: Code upload deposits are already held on the pallet account by try_upload_code + Self::set_base_deposit_hold( + &target_address, + &target_account, + &mut contract_info, + code_deposit, + )?; + AccountInfo::::insert_contract( &H160::from_slice(target.as_slice()), - contract_info, + contract_info.clone(), ); + Ok::<(), Error>(()) })?; Ok(Default::default()) diff --git a/testdata/default/revive/EtchTest.t.sol b/testdata/default/revive/EtchTest.t.sol index 691146fb6d0ee..0fd54dc09f63d 100644 --- a/testdata/default/revive/EtchTest.t.sol +++ b/testdata/default/revive/EtchTest.t.sol @@ -126,3 +126,51 @@ contract EtchTest is DSTest { assertEq(nested_call_result2, 3); } } + +// Simple contract that writes to storage +contract StorageWriter { + uint256 public value; + + function setValue(uint256 _value) external { + value = _value; + } +} + +contract MinimalStorageDeposit is DSTest { + Vm constant vm = Vm(address(bytes20(uint160(uint256(keccak256("hevm cheat code")))))); + + // Test that etch with pre-funded address preserves the visible balance + function testEtchPreservesBalance() public { + bytes memory code = vm.getDeployedCode("EtchTest.t.sol:StorageWriter"); + + // Fund an address with a specific balance BEFORE etching + address etched = address(0xABCDEF); + uint256 expectedBalance = 1 ether; + vm.deal(etched, expectedBalance); + assertEq(etched.balance, expectedBalance, "Initial balance should be 1 ether"); + + vm.etch(etched, code); + + assertEq(etched.balance, expectedBalance, "Balance should be preserved after etch"); + + StorageWriter(etched).setValue(42); + assertEq(StorageWriter(etched).value(), 42); + + assertEq(etched.balance, expectedBalance, "Balance should be preserved after storage write"); + } + + // Test that etch to unfunded address keeps balance at 0 + function testEtchUnfundedAddressZeroBalance() public { + bytes memory code = vm.getDeployedCode("EtchTest.t.sol:StorageWriter"); + + address etched = address(0x9999999999); + assertEq(etched.balance, 0, "Balance should be 0 before etch"); + + vm.etch(etched, code); + + assertEq(etched.balance, 0, "Balance should still be 0 after etch"); + + StorageWriter(etched).setValue(123); + assertEq(StorageWriter(etched).value(), 123); + } +} diff --git a/testdata/default/revive/Prank.t.sol b/testdata/default/revive/Prank.t.sol index 54a1f806edb33..85a2db0bb9bc1 100644 --- a/testdata/default/revive/Prank.t.sol +++ b/testdata/default/revive/Prank.t.sol @@ -710,5 +710,4 @@ contract FundPrankedAccountsReproTest is DSTest { assertEq(alice.balance, 5 ether, "alice balance should remain 5 ether"); vm.stopPrank(); } - }