Skip to main content

revive_strategy/
state.rs

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        // Set chain id in pallet-revive runtime.
108        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        // Set block number in pallet-revive runtime.
122        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                // First initialize and finalize the parent block to set up correct hashes.
139                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, &current_hash, &digest);
143                }
144
145                // Now finalize the new block to set up its hash.
146                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), &current_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        // Get block number in pallet-revive runtime.
159        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        // Set timestamp in pallet-revive runtime (milliseconds).
177        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        // Release any existing hold
208        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            // Decrease EVM balance by released amount (hold became free, so visible balance would
214            // increase)
215            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        // Create new hold with correct amount
225        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            // Update base deposit hold for both new and existing contracts
289            // Note: Code upload deposits are already held on the pallet account by try_upload_code
290            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}