1use alloy_primitives::{Address, B256, Bytes, FixedBytes, U256};
2use foundry_cheatcodes::{Error, Result};
3use polkadot_sdk::{
4 frame_support::traits::{
5 fungible::{InspectHold, MutateHold},
6 tokens::Precision,
7 },
8 pallet_revive::{
9 self, AccountId32Mapper, AccountInfo, AddressMapper, BytecodeType, ContractInfo,
10 ExecConfig, Executable, HoldReason, Pallet, ResourceMeter,
11 },
12 sp_core::{self, H160, H256},
13 sp_externalities::Externalities,
14 sp_io::TestExternalities,
15 sp_runtime::AccountId32,
16 sp_weights::Weight,
17};
18use revive_env::{Balances, BlockAuthor, ExtBuilder, NativeToEthRatio, Runtime, System, Timestamp};
19use std::{
20 fmt::Debug,
21 sync::{Arc, Mutex},
22};
23
24pub(crate) struct Inner {
25 pub externalities: TestExternalities,
26 pub depth: usize,
27}
28
29#[derive(Default)]
30pub struct TestEnv(pub(crate) Arc<Mutex<Inner>>);
31
32impl Default for Inner {
33 fn default() -> Self {
34 Self {
35 externalities: ExtBuilder::default()
36 .balance_genesis_config(vec![(
37 H160::from_low_u64_be(1),
38 1_000_000_000_000_000_000_000_000_000_u128,
39 )])
40 .build(),
41 depth: 0,
42 }
43 }
44}
45
46impl Debug for TestEnv {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 f.write_str("<Externalities>")
49 }
50}
51
52impl Clone for TestEnv {
53 fn clone(&self) -> Self {
54 let mut inner: Inner = Default::default();
55 inner.externalities.backend = self.0.lock().unwrap().externalities.as_backend();
56 inner.depth = self.0.lock().unwrap().depth;
57 Self(Arc::new(Mutex::new(inner)))
58 }
59}
60
61impl TestEnv {
62 pub fn shallow_clone(&self) -> Self {
63 Self(self.0.clone())
64 }
65
66 pub fn start_snapshotting(&mut self) {
67 let mut state = self.0.lock().unwrap();
68 state.depth += 1;
69 state.externalities.ext().storage_start_transaction();
70 }
71
72 pub fn revert(&mut self, depth: usize) {
73 let mut state = self.0.lock().unwrap();
74 while state.depth > depth + 1 {
75 state.externalities.ext().storage_rollback_transaction().unwrap();
76 state.depth -= 1;
77 }
78 state.externalities.ext().storage_rollback_transaction().unwrap();
79 state.externalities.ext().storage_start_transaction();
80 }
81
82 pub fn execute_with<R, F: FnOnce() -> R>(&mut self, f: F) -> R {
83 self.0.lock().unwrap().externalities.execute_with(f)
84 }
85
86 pub fn get_nonce(&mut self, account: Address) -> u32 {
87 self.0.lock().unwrap().externalities.execute_with(|| {
88 System::account_nonce(AccountId32Mapper::<Runtime>::to_fallback_account_id(
89 &H160::from_slice(account.as_slice()),
90 ))
91 })
92 }
93
94 pub fn set_nonce(&mut self, address: Address, nonce: u64) {
95 self.0.lock().unwrap().externalities.execute_with(|| {
96 let account_id = AccountId32Mapper::<Runtime>::to_fallback_account_id(
97 &H160::from_slice(address.as_slice()),
98 );
99
100 polkadot_sdk::frame_system::Account::<Runtime>::mutate(&account_id, |a| {
101 a.nonce = nonce.min(u32::MAX.into()).try_into().expect("shouldn't happen");
102 });
103 });
104 }
105
106 pub fn set_chain_id(&mut self, new_chain_id: u64) {
107 self.0.lock().unwrap().externalities.execute_with(|| {
109 <revive_env::Runtime as polkadot_sdk::pallet_revive::Config>::ChainId::set(
110 &new_chain_id,
111 );
112 });
113 }
114
115 pub fn set_block_number(
116 &mut self,
117 new_height: U256,
118 prev_new_height_hash: B256,
119 new_height_hash: B256,
120 ) -> U256 {
121 self.0.lock().unwrap().externalities.execute_with(|| {
123 let u64_max = U256::from(u64::MAX);
124 let clamped_height = if new_height > u64_max {
125 tracing::warn!(
126 block_number = ?new_height,
127 max = ?u64_max,
128 "Block number exceeds u64::MAX. Clamping to u64::MAX."
129 );
130 u64_max
131 } else {
132 new_height
133 };
134
135 let new_block_number: u64 = clamped_height.to();
136 let digest = System::digest();
137 if System::block_hash(new_block_number) == H256::zero() {
138 if new_block_number > 0 {
140 System::set_block_number(new_block_number - 1);
141 let current_hash = H256::from_slice(prev_new_height_hash.0.as_slice());
142 System::initialize(&new_block_number, ¤t_hash, &digest);
143 }
144
145 if new_block_number < u64::MAX {
147 System::set_block_number(new_block_number);
148 let current_hash = H256::from_slice(new_height_hash.0.as_slice());
149 System::initialize(&(new_block_number + 1), ¤t_hash, &digest);
150 }
151 }
152 System::set_block_number(new_block_number);
153 clamped_height
154 })
155 }
156
157 pub fn get_block_number(&mut self) -> U256 {
158 self.0.lock().unwrap().externalities.execute_with(|| U256::from(System::block_number()))
160 }
161
162 pub fn roll<DB: revm::Database + ?Sized>(
163 &mut self,
164 new_height: U256,
165 database: &mut DB,
166 ) -> U256 {
167 let block_num_u64 = new_height.saturating_to::<u64>();
168 let prev_block_hash =
169 database.block_hash(block_num_u64.saturating_sub(1)).unwrap_or_default();
170 let current_block_hash = database.block_hash(block_num_u64).unwrap_or_default();
171
172 self.set_block_number(new_height, prev_block_hash, current_block_hash)
173 }
174
175 pub fn set_timestamp(&mut self, new_timestamp: U256) -> U256 {
176 self.0.lock().unwrap().externalities.execute_with(|| {
178 let u64_max = U256::from(u64::MAX);
179 let clamped_timestamp = if new_timestamp > u64_max {
180 tracing::warn!(
181 timestamp = ?new_timestamp,
182 max = ?u64_max,
183 "Timestamp exceeds u64::MAX. Clamping to u64::MAX."
184 );
185 u64_max
186 } else {
187 new_timestamp
188 };
189
190 let timestamp_ms = clamped_timestamp.saturating_to::<u64>().saturating_mul(1000);
191 Timestamp::set_timestamp(timestamp_ms);
192 clamped_timestamp
193 })
194 }
195
196 fn set_base_deposit_hold(
197 target_address: &H160,
198 target_account: &AccountId32,
199 contract_info: &mut ContractInfo<Runtime>,
200 code_deposit: u128,
201 ) -> foundry_cheatcodes::Result {
202 contract_info.update_base_deposit(code_deposit);
203
204 let base_deposit: u128 = contract_info.storage_base_deposit();
205 let hold_reason: revive_env::RuntimeHoldReason = HoldReason::StorageDepositReserve.into();
206
207 let current_held = Balances::balance_on_hold(&hold_reason, target_account);
209 if current_held > 0 {
210 Balances::release(&hold_reason, target_account, current_held, Precision::BestEffort)
211 .map_err(|_| <&str as Into<Error>>::into("Could not release old hold"))?;
212
213 let current_evm_balance = Pallet::<Runtime>::evm_balance(target_address);
216 let release_wei = sp_core::U256::from(current_held)
217 .saturating_mul(sp_core::U256::from(NativeToEthRatio::get() as u128));
218 let adjusted_balance = current_evm_balance.saturating_sub(release_wei);
219 Pallet::<Runtime>::set_evm_balance(target_address, adjusted_balance).map_err(|_| {
220 <&str as Into<Error>>::into("Could not adjust balance after release")
221 })?;
222 }
223
224 if base_deposit > 0 {
226 let current_evm_balance = Pallet::<Runtime>::evm_balance(target_address);
227 let hold_wei = sp_core::U256::from(base_deposit)
228 .saturating_mul(sp_core::U256::from(NativeToEthRatio::get() as u128));
229 let new_evm_balance = current_evm_balance.saturating_add(hold_wei);
230
231 Pallet::<Runtime>::set_evm_balance(target_address, new_evm_balance)
232 .map_err(|_| <&str as Into<Error>>::into("Could not set balance for new hold"))?;
233
234 Balances::hold(&hold_reason, target_account, base_deposit)
235 .map_err(|_| <&str as Into<Error>>::into("Could not create new hold"))?;
236 }
237
238 Ok(Default::default())
239 }
240
241 pub fn etch_call(&mut self, target: &Address, new_runtime_code: &Bytes) -> Result {
242 self.0.lock().unwrap().externalities.execute_with(|| {
243 let target_address = H160::from_slice(target.as_slice());
244 let target_account =
245 AccountId32Mapper::<Runtime>::to_fallback_account_id(&target_address);
246
247 let code = new_runtime_code.to_vec();
248 let code_type =
249 if code.starts_with(b"PVM\0") { BytecodeType::Pvm } else { BytecodeType::Evm };
250 let contract_blob = Pallet::<Runtime>::try_upload_code(
251 Pallet::<Runtime>::account_id(),
252 code,
253 code_type,
254 &mut ResourceMeter::new(pallet_revive::TransactionLimits::WeightAndDeposit {
255 weight_limit: Weight::from_parts(10_000_000_000_000, 100_000_000),
256 deposit_limit: { 100_000_000_000_000 },
257 })
258 .unwrap(),
259 &ExecConfig::new_substrate_tx(),
260 )
261 .map_err(|_| <&str as Into<Error>>::into("Could not upload PVM code"))?;
262
263 let code_deposit = contract_blob.code_info().deposit();
264 let code_hash = *contract_blob.code_hash();
265
266 let mut contract_info = if let Some(contract_info) =
267 AccountInfo::<Runtime>::load_contract(&target_address)
268 {
269 contract_info
270 } else {
271 let contract_info = ContractInfo::<Runtime>::new(
272 &target_address,
273 System::account_nonce(&target_account),
274 code_hash,
275 )
276 .map_err(|err| {
277 tracing::error!("Could not create contract info: {:?}", err);
278 <&str as Into<Error>>::into("Could not create contract info")
279 })?;
280 System::inc_account_nonce(AccountId32Mapper::<Runtime>::to_fallback_account_id(
281 &target_address,
282 ));
283 contract_info
284 };
285
286 contract_info.code_hash = code_hash;
287
288 Self::set_base_deposit_hold(
291 &target_address,
292 &target_account,
293 &mut contract_info,
294 code_deposit,
295 )?;
296
297 AccountInfo::<Runtime>::insert_contract(
298 &H160::from_slice(target.as_slice()),
299 contract_info.clone(),
300 );
301
302 Ok::<(), Error>(())
303 })?;
304 Ok(Default::default())
305 }
306
307 pub fn get_storage(
308 &mut self,
309 target: Address,
310 slot: FixedBytes<32>,
311 ) -> Result<Option<Vec<u8>>, Error> {
312 let target_address_h160 = H160::from_slice(target.as_slice());
313 self.0
314 .lock()
315 .unwrap()
316 .externalities
317 .execute_with(|| {
318 pallet_revive::Pallet::<Runtime>::get_storage(target_address_h160, slot.into())
319 })
320 .map_err(|_| <&str as Into<Error>>::into("Could not set storage"))
321 }
322
323 pub fn set_storage(
324 &mut self,
325 target: Address,
326 slot: FixedBytes<32>,
327 value: FixedBytes<32>,
328 ) -> Result<(), Error> {
329 let target_address_h160 = H160::from_slice(target.as_slice());
330 self.0
331 .lock()
332 .unwrap()
333 .externalities
334 .execute_with(|| {
335 pallet_revive::Pallet::<Runtime>::set_storage(
336 target_address_h160,
337 slot.into(),
338 Some(value.to_vec()),
339 )
340 })
341 .map_err(|_| <&str as Into<Error>>::into("Could not set storage"))?;
342 Ok(())
343 }
344
345 pub fn set_balance(&mut self, address: Address, amount: U256) -> U256 {
346 let u128_max = U256::from(u128::MAX);
347 let clamped_amount = if amount > u128_max {
348 tracing::warn!(
349 address = ?address,
350 requested = ?amount,
351 actual = ?u128_max,
352 "Balance exceeds u128::MAX, clamping to u128::MAX. \
353 pallet-revive uses u128 for balances."
354 );
355 u128_max
356 } else {
357 amount
358 };
359
360 let amount_pvm = sp_core::U256::from_little_endian(&clamped_amount.as_le_bytes());
361
362 self.0.lock().unwrap().externalities.execute_with(|| {
363 let h160_addr = H160::from_slice(address.as_slice());
364 pallet_revive::Pallet::<Runtime>::set_evm_balance(&h160_addr, amount_pvm)
365 .expect("failed to set evm balance");
366 });
367
368 clamped_amount
369 }
370
371 pub fn get_balance(&mut self, address: Address) -> U256 {
372 U256::from_limbs(
373 self.0
374 .lock()
375 .unwrap()
376 .externalities
377 .execute_with(|| {
378 let h160_addr = H160::from_slice(address.as_slice());
379 pallet_revive::Pallet::<Runtime>::evm_balance(&h160_addr)
380 })
381 .0,
382 )
383 }
384
385 pub fn set_block_author(&mut self, new_author: Address) {
386 self.0.lock().unwrap().externalities.execute_with(|| {
387 let account_id32 = AccountId32Mapper::<Runtime>::to_fallback_account_id(
388 &H160::from_slice(new_author.as_slice()),
389 );
390 BlockAuthor::set(&account_id32);
391 });
392 }
393
394 pub fn set_blockhash(&mut self, block_number: u64, block_hash: FixedBytes<32>) {
395 self.0.lock().unwrap().externalities.execute_with(|| {
396 use polkadot_sdk::frame_system::BlockHash;
397
398 let hash = sp_core::H256::from_slice(block_hash.as_slice());
399 BlockHash::<Runtime>::insert::<u64, _>(block_number, hash);
400 });
401 }
402
403 pub fn is_contract(&self, address: Address) -> bool {
404 self.0.lock().unwrap().externalities.execute_with(|| {
405 AccountInfo::<Runtime>::load_contract(&H160::from_slice(address.as_slice())).is_some()
406 })
407 }
408}