Skip to main content

revive_strategy/cheatcodes/
mod.rs

1mod mock_handler;
2
3use alloy_primitives::{Address, B256, Bytes, Log, hex, ruint::aliases::U256};
4use alloy_rpc_types::BlobTransactionSidecar;
5use alloy_sol_types::SolValue;
6use foundry_cheatcodes::{
7    Broadcast, BroadcastableTransactions, CheatcodeInspectorStrategy,
8    CheatcodeInspectorStrategyContext, CheatcodeInspectorStrategyRunner, CheatsConfig, CheatsCtxt,
9    CommonCreateInput, DynCheatcode, Ecx, EvmCheatcodeInspectorStrategyRunner, Result,
10    Vm::{
11        AccountAccessKind, chainIdCall, coinbaseCall, dealCall, etchCall, getNonce_0Call, loadCall,
12        polkadot_0Call, polkadot_1Call, polkadotSkipCall, resetNonceCall,
13        revertToStateAndDeleteCall, revertToStateCall, rollCall, setBlockhashCall, setNonceCall,
14        setNonceUnsafeCall, snapshotStateCall, storeCall, warpCall,
15    },
16    journaled_account, precompile_error,
17};
18
19use foundry_compilers::resolc::dual_compiled_contracts::DualCompiledContracts;
20use foundry_evm::constants::CHEATCODE_ADDRESS;
21use revive_env::{Runtime, System, Timestamp};
22use std::{
23    any::{Any, TypeId},
24    sync::Arc,
25};
26use tracing::warn;
27
28use alloy_eips::eip7702::SignedAuthorization;
29use polkadot_sdk::{
30    pallet_revive::{
31        self, AccountId32Mapper, AccountInfo, AddressMapper, BytecodeType, Code, ContractInfo,
32        DebugSettings, ExecConfig, Executable, Pallet, ResourceMeter, evm::CallTrace,
33    },
34    polkadot_sdk_frame::prelude::OriginFor,
35    sp_core::{self, H160},
36    sp_weights::Weight,
37};
38
39use crate::{
40    cheatcodes::mock_handler::MockHandlerImpl,
41    state::TestEnv,
42    tracing::{Tracer, storage_tracer::AccountAccess},
43};
44use foundry_cheatcodes::Vm::{AccountAccess as FAccountAccess, ChainInfo};
45use polkadot_sdk::pallet_revive::tracing::Tracing;
46
47use revm::{
48    bytecode::opcode as op,
49    context::{CreateScheme, JournalTr},
50    interpreter::{
51        CallInputs, CallOutcome, CallScheme, CreateOutcome, Gas, InstructionResult, Interpreter,
52        InterpreterResult, interpreter_types::Jumps,
53    },
54    state::Bytecode,
55};
56pub trait PvmCheatcodeInspectorStrategyBuilder {
57    fn new_pvm(
58        dual_compiled_contracts: DualCompiledContracts,
59        runtime_mode: crate::ReviveRuntimeMode,
60        externalities: TestEnv,
61    ) -> Self;
62}
63impl PvmCheatcodeInspectorStrategyBuilder for CheatcodeInspectorStrategy {
64    // Creates a new PVM strategy
65    fn new_pvm(
66        dual_compiled_contracts: DualCompiledContracts,
67        runtime_mode: crate::ReviveRuntimeMode,
68        externalities: TestEnv,
69    ) -> Self {
70        Self {
71            runner: &PvmCheatcodeInspectorStrategyRunner,
72            context: Box::new(PvmCheatcodeInspectorStrategyContext::new(
73                dual_compiled_contracts,
74                runtime_mode,
75                externalities,
76            )),
77        }
78    }
79}
80
81/// Controls the automatic migration to pallet-revive during test execution.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
83pub enum ReviveStartupMigration {
84    /// Defer database migration to a later execution point.
85    /// This is the initial state - waiting for the test contract to be deployed.
86    Defer,
87    /// Allow database migration to pallet-revive (EVM or PVM mode).
88    /// Set by `base_contract_deployed()` when the test contract is deployed.
89    #[default]
90    Allow,
91    /// Database migration has already been performed.
92    /// Prevents redundant migrations.
93    Done,
94}
95
96impl ReviveStartupMigration {
97    /// Check if startup migration is allowed
98    pub fn is_allowed(&self) -> bool {
99        matches!(self, Self::Allow)
100    }
101
102    /// Allow migrating the database to PVM storage
103    pub fn allow(&mut self) {
104        *self = Self::Allow;
105    }
106
107    /// Mark the migration as completed
108    pub fn done(&mut self) {
109        *self = Self::Done;
110    }
111}
112/// PVM-specific strategy context.
113#[derive(Debug, Default, Clone)]
114pub struct PvmCheatcodeInspectorStrategyContext {
115    /// Whether we're currently using pallet-revive (migrated from REVM)
116    pub using_revive: bool,
117    /// When in Polkadot context, execute the next CALL or CREATE in the EVM instead.
118    pub skip_revive: bool,
119    /// Any contracts that were deployed in `skip_revive` step.
120    /// This makes it easier to dispatch calls to any of these addresses in revive context,
121    /// directly to EVM. Alternatively, we'd need to add `vm.polkadotSkip()` to these calls
122    /// manually.
123    pub skip_revive_addresses: std::collections::HashSet<Address>,
124    /// Records the next create address for `skip_pvm_addresses`.
125    pub record_next_create_address: bool,
126    /// Controls automatic migration to pallet-revive
127    pub revive_startup_migration: ReviveStartupMigration,
128    pub dual_compiled_contracts: DualCompiledContracts,
129    /// Runtime backend mode when using pallet-revive (PVM or EVM)
130    pub runtime_mode: crate::ReviveRuntimeMode,
131    pub remove_recorded_access_at: Option<usize>,
132    pub externalities: TestEnv,
133}
134
135impl PvmCheatcodeInspectorStrategyContext {
136    pub fn new(
137        dual_compiled_contracts: DualCompiledContracts,
138        runtime_mode: crate::ReviveRuntimeMode,
139        externalities: TestEnv,
140    ) -> Self {
141        Self {
142            // Start in REVM mode by default
143            using_revive: false,
144            skip_revive: false,
145            skip_revive_addresses: Default::default(),
146            record_next_create_address: Default::default(),
147            // Will be set to Allow when test contract deploys
148            revive_startup_migration: ReviveStartupMigration::Defer,
149            dual_compiled_contracts,
150            runtime_mode,
151            remove_recorded_access_at: None,
152            externalities,
153        }
154    }
155}
156
157impl CheatcodeInspectorStrategyContext for PvmCheatcodeInspectorStrategyContext {
158    fn new_cloned(&self) -> Box<dyn CheatcodeInspectorStrategyContext> {
159        Box::new(self.clone())
160    }
161
162    fn as_any_mut(&mut self) -> &mut dyn Any {
163        self
164    }
165
166    fn as_any_ref(&self) -> &dyn Any {
167        self
168    }
169}
170
171/// Implements [CheatcodeInspectorStrategyRunner] for PVM.
172#[derive(Debug, Default, Clone)]
173pub struct PvmCheatcodeInspectorStrategyRunner;
174
175impl PvmCheatcodeInspectorStrategyRunner {
176    fn append_recorded_accesses(
177        &self,
178        state: &mut foundry_cheatcodes::Cheatcodes,
179        ecx: Ecx<'_, '_, '_>,
180        account_accesses: Vec<AccountAccess>,
181    ) {
182        let ctx: &mut PvmCheatcodeInspectorStrategyContext =
183            get_context_ref_mut(state.strategy.context.as_mut());
184        // account_accesses.sort_by(|a, b| a.depth.cmp(&b.depth));
185        if state.recording_accesses {
186            for record in &account_accesses {
187                for r in &record.storage_accesses {
188                    if !r.isWrite {
189                        state.accesses.record_read(
190                            Address::from(record.account.0),
191                            alloy_primitives::U256::from_be_slice(r.slot.clone().as_slice()),
192                        );
193                    } else {
194                        state.accesses.record_write(
195                            Address::from(record.account.0),
196                            alloy_primitives::U256::from_be_slice(r.slot.clone().as_slice()),
197                        );
198                    }
199                }
200            }
201        }
202
203        if let Some(recorded_account_diffs_stack) = state.recorded_account_diffs_stack.as_mut() {
204            // A duplicate entry is inserted on call/create start by the revm, and updated on
205            // call/create end.
206            //
207            // If we are inside a nested call (stack depth > 1), the placeholder
208            // lives in the *parent* frame.  Its index will be exactly the current
209            // length of that parent vector (`len()`), so we record that length.
210            //
211            // If we are at the root (depth == 1), the placeholder is already the
212            // last element of the root vector.  We therefore record `len() - 1`.
213            //
214            // `zksync_fix_recorded_accesses()` uses this index later to drop the
215            // single duplicate.
216            //
217            // TODO(zk): This is currently a hack, as account access recording is
218            // done in 4 parts - create/create_end and call/call_end. And these must all be
219            // moved to strategy.
220            let stack_insert_index = if recorded_account_diffs_stack.len() > 1 {
221                recorded_account_diffs_stack
222                    .get(recorded_account_diffs_stack.len() - 2)
223                    .map_or(0, Vec::len)
224            } else {
225                // `len() - 1`
226                recorded_account_diffs_stack.first().map_or(0, |v| v.len().saturating_sub(1))
227            };
228
229            if let Some(last) = recorded_account_diffs_stack.last_mut() {
230                ctx.remove_recorded_access_at = Some(stack_insert_index);
231                for record in account_accesses {
232                    let _ = ecx
233                        .journaled_state
234                        .load_account(Address::from(record.account.0))
235                        .expect("failed to load account");
236
237                    let access = FAccountAccess {
238                        chainInfo: ChainInfo {
239                            forkId: ecx
240                                .journaled_state
241                                .database
242                                .active_fork_id()
243                                .unwrap_or_default(),
244                            chainId: U256::from(ecx.cfg.chain_id),
245                        },
246                        accessor: Address::from(record.accessor.0),
247                        account: Address::from(record.account.0),
248                        kind: record.kind,
249                        initialized: record.initialized,
250                        oldBalance: U256::from_limbs(record.old_balance.0),
251                        newBalance: ecx
252                            .journaled_state
253                            .account(Address::from(record.account.0))
254                            .info
255                            .balance,
256                        value: U256::from_limbs(record.value.0),
257                        data: record.data.clone(),
258                        reverted: record.reverted,
259                        deployedCode: if matches!(record.kind, AccountAccessKind::Create) {
260                            ctx.externalities
261                                .execute_with(|| {
262                                    Some(Bytes::from(Pallet::<Runtime>::code(&H160::from(
263                                        record.account.0,
264                                    ))))
265                                })
266                                .unwrap_or_else(|| Bytes::from(record.data.0.to_vec()))
267                        } else {
268                            Bytes::new()
269                        },
270                        storageAccesses: record.storage_accesses,
271                        depth: record.depth,
272                    };
273                    last.push(access);
274                }
275            }
276        }
277    }
278}
279
280impl CheatcodeInspectorStrategyRunner for PvmCheatcodeInspectorStrategyRunner {
281    fn apply_full(
282        &self,
283        cheatcode: &dyn foundry_cheatcodes::DynCheatcode,
284        ccx: &mut CheatsCtxt<'_, '_, '_, '_>,
285        executor: &mut dyn foundry_cheatcodes::CheatcodesExecutor,
286    ) -> Result {
287        fn is<T: std::any::Any>(t: TypeId) -> bool {
288            TypeId::of::<T>() == t
289        }
290        let ctx: &mut PvmCheatcodeInspectorStrategyContext =
291            get_context_ref_mut(ccx.state.strategy.context.as_mut());
292        let using_revive = ctx.using_revive;
293        match cheatcode.as_any().type_id() {
294            t if is::<polkadot_0Call>(t) => {
295                // polkadot(bool enable, string backend)
296                tracing::info!(cheatcode = ?cheatcode.as_debug(), using_revive = ?using_revive);
297                let polkadot_0Call { enable, backend } = cheatcode.as_any().downcast_ref().unwrap();
298                let ctx: &mut PvmCheatcodeInspectorStrategyContext =
299                    get_context_ref_mut(ccx.state.strategy.context.as_mut());
300                handle_polkadot_call(ctx, ccx.ecx, *enable, backend)
301            }
302            t if is::<polkadot_1Call>(t) => {
303                // polkadot(bool enable) - auto-detect backend
304                tracing::info!(cheatcode = ?cheatcode.as_debug(), using_revive = ?using_revive);
305                let polkadot_1Call { enable } = cheatcode.as_any().downcast_ref().unwrap();
306                let ctx: &mut PvmCheatcodeInspectorStrategyContext =
307                    get_context_ref_mut(ccx.state.strategy.context.as_mut());
308                handle_polkadot_call(ctx, ccx.ecx, *enable, "")
309            }
310            t if is::<polkadotSkipCall>(t) => {
311                tracing::info!(cheatcode = ?cheatcode.as_debug(), using_revive = ?using_revive);
312                let polkadotSkipCall { .. } = cheatcode.as_any().downcast_ref().unwrap();
313                let ctx = get_context_ref_mut(ccx.state.strategy.context.as_mut());
314                ctx.skip_revive = true;
315                Ok(Default::default())
316            }
317            t if using_revive && is::<dealCall>(t) => {
318                tracing::info!(cheatcode = ?cheatcode.as_debug() , using_revive = ?using_revive);
319                let dealCall { account, newBalance } = cheatcode.as_any().downcast_ref().unwrap();
320
321                let clamped_balance = ctx.externalities.set_balance(*account, *newBalance);
322                let clamped_deal = dealCall { account: *account, newBalance: clamped_balance };
323                clamped_deal.dyn_apply(ccx, executor)
324            }
325            t if using_revive && is::<setNonceCall>(t) => {
326                tracing::info!(cheatcode = ?cheatcode.as_debug() , using_revive = ?using_revive);
327
328                let &setNonceCall { account, newNonce } =
329                    cheatcode.as_any().downcast_ref().unwrap();
330                ctx.externalities.set_nonce(account, newNonce);
331
332                cheatcode.dyn_apply(ccx, executor)
333            }
334            t if using_revive && is::<setNonceUnsafeCall>(t) => {
335                tracing::info!(cheatcode = ?cheatcode.as_debug() , using_revive = ?using_revive);
336
337                let &setNonceUnsafeCall { account, newNonce } =
338                    cheatcode.as_any().downcast_ref().unwrap();
339                ctx.externalities.set_nonce(account, newNonce);
340
341                cheatcode.dyn_apply(ccx, executor)
342            }
343            t if using_revive && is::<resetNonceCall>(t) => {
344                tracing::info!(cheatcode = ?cheatcode.as_debug() , using_revive = ?using_revive);
345                let &resetNonceCall { account } = cheatcode.as_any().downcast_ref().unwrap();
346                ctx.externalities.set_nonce(account, 0);
347                cheatcode.dyn_apply(ccx, executor)
348            }
349            t if using_revive && is::<getNonce_0Call>(t) => {
350                tracing::info!(cheatcode = ?cheatcode.as_debug() , using_revive = ?using_revive);
351                let &getNonce_0Call { account } = cheatcode.as_any().downcast_ref().unwrap();
352                let ctx = get_context_ref_mut(ccx.state.strategy.context.as_mut());
353                let nonce = ctx.externalities.get_nonce(account);
354                Ok(u64::from(nonce).abi_encode())
355            }
356            t if using_revive && is::<rollCall>(t) => {
357                let &rollCall { newHeight } = cheatcode.as_any().downcast_ref().unwrap();
358                let clamped_height =
359                    ctx.externalities.roll(newHeight, &mut *ccx.ecx.journaled_state.database);
360
361                rollCall { newHeight: clamped_height }.dyn_apply(ccx, executor)
362            }
363            t if using_revive && is::<snapshotStateCall>(t) => {
364                ctx.externalities.start_snapshotting();
365                cheatcode.dyn_apply(ccx, executor)
366            }
367            t if using_revive && is::<revertToStateAndDeleteCall>(t) => {
368                let &revertToStateAndDeleteCall { snapshotId } =
369                    cheatcode.as_any().downcast_ref().unwrap();
370
371                ctx.externalities.revert(snapshotId.try_into().unwrap());
372                cheatcode.dyn_apply(ccx, executor)
373            }
374            t if using_revive && is::<revertToStateCall>(t) => {
375                let &revertToStateCall { snapshotId } = cheatcode.as_any().downcast_ref().unwrap();
376
377                ctx.externalities.revert(snapshotId.try_into().unwrap());
378                cheatcode.dyn_apply(ccx, executor)
379            }
380            t if using_revive && is::<warpCall>(t) => {
381                let &warpCall { newTimestamp } = cheatcode.as_any().downcast_ref().unwrap();
382
383                let clamped_timestamp = ctx.externalities.set_timestamp(newTimestamp);
384
385                warpCall { newTimestamp: clamped_timestamp }.dyn_apply(ccx, executor)
386            }
387
388            t if using_revive && is::<chainIdCall>(t) => {
389                let &chainIdCall { newChainId } = cheatcode.as_any().downcast_ref().unwrap();
390
391                tracing::info!(cheatcode = ?cheatcode.as_debug() , using_revive = ?using_revive);
392                ctx.externalities.set_chain_id(newChainId.to());
393
394                cheatcode.dyn_apply(ccx, executor)
395            }
396            t if using_revive && is::<coinbaseCall>(t) => {
397                let &coinbaseCall { newCoinbase } = cheatcode.as_any().downcast_ref().unwrap();
398
399                tracing::info!(cheatcode = ?cheatcode.as_debug() , using_revive = ?using_revive);
400                ctx.externalities.set_block_author(newCoinbase);
401
402                cheatcode.dyn_apply(ccx, executor)
403            }
404            t if is::<setBlockhashCall>(t) => {
405                let &setBlockhashCall { blockNumber, blockHash } =
406                    cheatcode.as_any().downcast_ref().unwrap();
407
408                tracing::info!(cheatcode = ?cheatcode.as_debug(), using_revive = ?using_revive);
409                let u64_max: U256 = U256::from(u64::MAX);
410                let clamped_block_number = if blockNumber > u64_max {
411                    tracing::warn!(
412                        blockNumber = ?blockNumber,
413                        max = ?u64_max,
414                        "Block number exceeds u64::MAX. Clamping to u64::MAX."
415                    );
416                    u64_max
417                } else {
418                    blockNumber
419                };
420
421                // Validate blockNumber is not in the future
422                let current_block = ctx.externalities.get_block_number();
423                if clamped_block_number > current_block {
424                    return Err(foundry_cheatcodes::Error::from(
425                        "block number must be less than or equal to the current block number",
426                    ));
427                }
428
429                ctx.externalities.set_blockhash(clamped_block_number.to(), blockHash);
430
431                setBlockhashCall { blockNumber: clamped_block_number, blockHash }
432                    .dyn_apply(ccx, executor)
433            }
434            t if using_revive && is::<etchCall>(t) => {
435                let etchCall { target, newRuntimeBytecode } =
436                    cheatcode.as_any().downcast_ref().unwrap();
437
438                if ccx.is_precompile(target) {
439                    return Err(precompile_error(target));
440                }
441
442                let ctx = get_context_ref_mut(ccx.state.strategy.context.as_mut());
443                ctx.externalities.etch_call(target, newRuntimeBytecode)?;
444
445                cheatcode.dyn_apply(ccx, executor)
446            }
447
448            t if is::<etchCall>(t) => {
449                let etchCall { target, newRuntimeBytecode: _ } =
450                    cheatcode.as_any().downcast_ref().unwrap();
451                // Etch could be called from the test contract constructor, so we allow it
452                // even if we're not yet using revive yet and mark the target as persistent, so
453                // the bytecode gets persisted.
454                ccx.ecx.journaled_state.database.add_persistent_account(*target);
455
456                cheatcode.dyn_apply(ccx, executor)
457            }
458            t if using_revive && is::<loadCall>(t) => {
459                tracing::info!(cheatcode = ?cheatcode.as_debug() , using_revive = ?using_revive);
460                let &loadCall { target, slot } = cheatcode.as_any().downcast_ref().unwrap();
461
462                // Check if target is the test contract - if so, read from REVM state instead
463                if ccx
464                    .ecx
465                    .journaled_state
466                    .database
467                    .get_test_contract_address()
468                    .map(|addr| target == addr)
469                    .unwrap_or_default()
470                {
471                    cheatcode.dyn_apply(ccx, executor)
472                } else {
473                    let ctx = get_context_ref_mut(ccx.state.strategy.context.as_mut());
474                    let storage_value = ctx.externalities.get_storage(target, slot)?;
475                    let result = storage_value.map(|b| B256::from_slice(&b)).unwrap_or(B256::ZERO);
476                    Ok(result.abi_encode())
477                }
478            }
479            t if using_revive && is::<storeCall>(t) => {
480                tracing::info!(cheatcode = ?cheatcode.as_debug() , using_revive = ?using_revive);
481                let &storeCall { target, slot, value } = cheatcode.as_any().downcast_ref().unwrap();
482                if ccx.is_precompile(&target) {
483                    return Err(precompile_error(&target));
484                }
485                let ctx = get_context_ref_mut(ccx.state.strategy.context.as_mut());
486                if target != CHEATCODE_ADDRESS {
487                    ctx.externalities.set_storage(target, slot, value)?;
488                }
489                cheatcode.dyn_apply(ccx, executor)
490            }
491            // Not custom, just invoke the default behavior
492            _ => cheatcode.dyn_apply(ccx, executor),
493        }
494    }
495
496    fn base_contract_deployed(&self, ctx: &mut dyn CheatcodeInspectorStrategyContext) {
497        let ctx = get_context_ref_mut(ctx);
498
499        tracing::debug!("allowing startup PVM migration");
500        ctx.revive_startup_migration.allow();
501    }
502
503    fn record_broadcastable_create_transactions(
504        &self,
505        _ctx: &mut dyn CheatcodeInspectorStrategyContext,
506        config: Arc<CheatsConfig>,
507        input: &dyn CommonCreateInput,
508        ecx_inner: Ecx<'_, '_, '_>,
509        broadcast: &Broadcast,
510        broadcastable_transactions: &mut BroadcastableTransactions,
511    ) {
512        // Use EVM implementation for now
513        // Only intercept PVM-specific calls when needed in future implementations
514        EvmCheatcodeInspectorStrategyRunner.record_broadcastable_create_transactions(
515            _ctx,
516            config,
517            input,
518            ecx_inner,
519            broadcast,
520            broadcastable_transactions,
521        );
522    }
523
524    fn record_broadcastable_call_transactions(
525        &self,
526        _ctx: &mut dyn CheatcodeInspectorStrategyContext,
527        config: Arc<CheatsConfig>,
528        call: &CallInputs,
529        ecx_inner: Ecx<'_, '_, '_>,
530        broadcast: &Broadcast,
531        broadcastable_transactions: &mut BroadcastableTransactions,
532        active_delegations: Vec<SignedAuthorization>,
533        active_blob_sidecar: Option<BlobTransactionSidecar>,
534    ) {
535        // Use EVM implementation for now
536        // Only intercept PVM-specific calls when needed in future implementations
537        EvmCheatcodeInspectorStrategyRunner.record_broadcastable_call_transactions(
538            _ctx,
539            config,
540            call,
541            ecx_inner,
542            broadcast,
543            broadcastable_transactions,
544            active_delegations,
545            active_blob_sidecar,
546        );
547    }
548
549    fn post_initialize_interp(
550        &self,
551        ctx: &mut dyn CheatcodeInspectorStrategyContext,
552        _interpreter: &mut Interpreter,
553        ecx: Ecx<'_, '_, '_>,
554    ) {
555        let ctx = get_context_ref_mut(ctx);
556
557        if ctx.revive_startup_migration.is_allowed() && !ctx.using_revive {
558            tracing::info!("startup pallet-revive migration initiated");
559            select_revive(ctx, ecx, true);
560            ctx.revive_startup_migration.done();
561            tracing::info!("startup pallet-revive migration completed");
562        }
563    }
564
565    fn pre_step_end(
566        &self,
567        ctx: &mut dyn CheatcodeInspectorStrategyContext,
568        interpreter: &mut Interpreter,
569        _ecx: Ecx<'_, '_, '_>,
570    ) -> bool {
571        let ctx = get_context_ref_mut(ctx);
572
573        if !ctx.using_revive {
574            return false;
575        }
576
577        let address = match interpreter.bytecode.opcode() {
578            op::SELFBALANCE => interpreter.input.target_address,
579            op::BALANCE => {
580                if interpreter.stack.is_empty() {
581                    return true;
582                }
583
584                Address::from_word(B256::from(unsafe { interpreter.stack.pop_unsafe() }))
585            }
586            _ => return true,
587        };
588
589        let balance = ctx.externalities.get_balance(address);
590        tracing::info!(operation = "get_balance" , using_revive = ?ctx.using_revive, target = ?address, balance = ?balance);
591
592        // Skip the current BALANCE instruction since we've already handled it
593        if interpreter.stack.push(balance) {
594            interpreter.bytecode.relative_jump(1);
595        } else {
596            // stack overflow; nothing else to do here
597        }
598
599        false // Let EVM handle all operations
600    }
601}
602
603fn handle_polkadot_call(
604    ctx: &mut PvmCheatcodeInspectorStrategyContext,
605    data: Ecx<'_, '_, '_>,
606    enable: bool,
607    backend: &str,
608) -> Result {
609    if enable {
610        // Empty string is only used internally by polkadot_1Call (single-param variant) for
611        // auto-detection
612        let target_mode = match backend.to_lowercase().as_str() {
613            "" => ctx.runtime_mode, // Auto-detect: use base mode from CLI
614            "evm" => crate::ReviveRuntimeMode::Evm,
615            "pvm" => {
616                tracing::warn!(
617                    "Using 'pvm' backend is an experimental feature and may lead to unexpected behavior in tests."
618                );
619                crate::ReviveRuntimeMode::Pvm
620            }
621            _ => return Err(format!("Invalid backend: '{backend}'. Use 'evm' or 'pvm'").into()),
622        };
623
624        // When explicit backend is provided, allow switching between EVM/PVM backends
625        let is_backend_switch = target_mode != ctx.runtime_mode && ctx.using_revive;
626
627        // Validate we're running with --polkadot and --resolc flags if switching to PVM
628        // If dual_compiled_contracts is empty, return error
629        if is_backend_switch
630            && ctx.dual_compiled_contracts.is_empty()
631            && target_mode == crate::ReviveRuntimeMode::Pvm
632        {
633            return Err(
634                "Backend switching to PVM requires running tests with --polkadot and --resolc flags".into(),
635            );
636        }
637        ctx.runtime_mode = target_mode;
638        if !is_backend_switch {
639            // Migrate to the target mode (from standard EVM to Polkadot)
640            select_revive(ctx, data, false);
641        }
642    } else if ctx.using_revive {
643        // Switching BACK to Foundry EVM
644        select_evm(ctx, data);
645    }
646    Ok(Default::default())
647}
648
649fn select_revive(
650    ctx: &mut PvmCheatcodeInspectorStrategyContext,
651    data: Ecx<'_, '_, '_>,
652    migrate_all: bool,
653) {
654    if ctx.using_revive {
655        tracing::info!("already using pallet-revive");
656        return;
657    }
658
659    tracing::info!("switching to pallet-revive ({} mode)", ctx.runtime_mode);
660    ctx.using_revive = true;
661
662    let block_number = data.block.number;
663    let timestamp = data.block.timestamp;
664    ctx.externalities.set_timestamp(timestamp);
665    ctx.externalities.roll(block_number, &mut *data.journaled_state.database);
666
667    ctx.externalities.execute_with(||{
668            // Enable debug mode to bypass EIP-170 size checks during testing
669            if data.cfg.limit_contract_code_size == Some(usize::MAX) {
670                let debug_settings = DebugSettings::new(true, true, true);
671                debug_settings.write_to_storage::<Runtime>();
672            }
673            <revive_env::Runtime as polkadot_sdk::pallet_revive::Config>::ChainId::set(
674                &data.cfg.chain_id,
675            );
676            let test_contract_addr = data.journaled_state.database.get_test_contract_address();
677            let mut accounts = data.journaled_state.database.persistent_accounts().clone();
678            accounts.insert(data.tx.caller);
679
680            if migrate_all {
681                // Migrate all cached contracts
682                accounts.extend(
683                    data.journaled_state
684                        .database
685                        .cached_accounts()
686                );
687            }
688
689            for address in accounts {
690                tracing::info!("Migrating account {:?} (is_test_contract: {})", address, test_contract_addr == Some(address)); 
691                let acc = data.journaled_state.load_account(address).expect("failed to load account");
692
693                let amount = acc.info.balance;
694                let nonce = acc.info.nonce;
695                let account = H160::from_slice(address.as_slice());
696                let account_id =
697                    AccountId32Mapper::<Runtime>::to_fallback_account_id(&account);
698
699                let u128_max: U256 = U256::from(u128::MAX);
700                let clamped_amount = if amount > u128_max {
701                    tracing::info!(
702                        address = ?address,
703                        requested = ?amount,
704                        actual = ?u128_max,
705                        "Migration: balance exceeds u128::MAX, clamping to u128::MAX. \
706                         pallet-revive uses u128 for balances."
707                    );
708                    u128_max
709                } else {
710                    amount
711                };
712
713                let amount_pvm = sp_core::U256::from_little_endian(&clamped_amount.as_le_bytes());
714                Pallet::<Runtime>::set_evm_balance(&account, amount_pvm)
715                    .expect("failed to set evm balance");
716
717                polkadot_sdk::frame_system::Account::<Runtime>::mutate(&account_id, |a| {
718                    a.nonce = nonce.min(u32::MAX.into()).try_into().expect("shouldn't happen");
719                });
720
721                if let Some(bytecode) = acc.info.code.as_ref() {
722                    let account_h160 = H160::from_slice(address.as_slice());
723
724                    // Skip if contract already exists in pallet-revive
725                    if AccountInfo::<Runtime>::load_contract(&account_h160).is_none() {
726                        // Find the matching dual-compiled contract by EVM bytecode
727                        if let Some((_, contract)) = ctx.dual_compiled_contracts
728                            .find_by_evm_deployed_bytecode_with_immutables(bytecode.original_byte_slice())
729                        {
730                            // Test contract should always use EVM bytecode, even in PVM mode
731                            // TODO check why this is needed now
732                            let effective_runtime_mode = if test_contract_addr == Some(address) {
733                                crate::ReviveRuntimeMode::Evm
734                            } else {
735                                ctx.runtime_mode
736                            };
737                            let (code_bytes, immutable_data, code_type) = match effective_runtime_mode {
738                                crate::ReviveRuntimeMode::Pvm => {
739                                    let immutable_data = contract.evm_immutable_references
740                                        .as_ref()
741                                        .map(|immutable_refs| {
742                                            let evm_bytecode = bytecode.original_byte_slice();
743
744                                            // Collect all immutable bytes from their scattered offsets
745                                            immutable_refs
746                                                .values().filter_map(|offsets| offsets.first())
747                                                .flat_map(|offset| {
748                                                    let start = offset.start as usize;
749                                                    let end = start + offset.length as usize;
750                                                    evm_bytecode.get(start..end).unwrap_or_else(|| panic!("Immutable offset out of bounds: address={:?}, offset={}..{}, bytecode_len={}",
751                                                        address, start, end, evm_bytecode.len())).iter().rev()
752                                                })
753                                                .copied()
754                                                .collect::<Vec<u8>>()
755                                        });
756                                    (contract.resolc_deployed_bytecode.as_bytes().map(|b| b.to_vec()),immutable_data, BytecodeType::Pvm)
757                                },
758                                crate::ReviveRuntimeMode::Evm => {
759                                    (Some(bytecode.bytecode().to_vec()), None, BytecodeType::Evm)
760                                },
761                            };
762
763                            if let Some(code_bytes) = code_bytes {
764                                let upload_result = Pallet::<Runtime>::try_upload_code(
765                                    Pallet::<Runtime>::account_id(),
766                                    code_bytes.clone(),
767                                    code_type,
768                                    &mut ResourceMeter::new(pallet_revive::TransactionLimits::WeightAndDeposit {
769                                        weight_limit: Weight::from_parts(10_000_000_000_000, 100_000_000),
770                                        deposit_limit: 100_000_000_000_000,
771                                    })
772                                    .unwrap(),
773                                    &ExecConfig::new_substrate_tx(),
774                                );
775                                match upload_result {
776                                    Ok(upload_res) => {
777                                        let code_hash = upload_res.code_hash().to_owned();
778                                        let contract_info = ContractInfo::<Runtime>::new(&account_h160, nonce as u32, code_hash)
779                                            .expect("Failed to create contract info");
780                                        AccountInfo::<Runtime>::insert_contract(&account_h160, contract_info);
781                                        if let Some(data) = immutable_data.and_then(|immutables| immutables.try_into().ok())
782                                        {
783                                            Pallet::<Runtime>::set_immutables(account_h160, data).expect("Failed to migrate immutables");
784                                        }
785                                    }
786                                    Err(err) => {
787                                        tracing::warn!(
788                                            address = ?address,
789                                            runtime_mode = ?ctx.runtime_mode,
790                                            bytecode_len = code_bytes.len(),
791                                            error = ?err,
792                                            "Failed to upload bytecode to pallet-revive, skipping migration"
793                                        );
794                                    }
795                                }
796                            } else {
797                                tracing::info!(
798                                    address = ?address,
799                                    "no PVM equivalent found for EVM bytecode, skipping migration"
800                                );
801                            }
802                        } else {
803                            tracing::info!("Setting evm bytecode stored in account {:?} balance: {:?}", address, amount);
804                            // Even if no dual-compiled contract is found, we still upload the existing bytecode because it might be some EVM bytecode that got etched earlier.
805                            let code_bytes = bytecode.original_byte_slice().to_vec();
806                            let upload_result = Pallet::<Runtime>::try_upload_code(
807                                Pallet::<Runtime>::account_id(),
808                                code_bytes.clone(),
809                                BytecodeType::Evm,
810                                &mut ResourceMeter::new(pallet_revive::TransactionLimits::WeightAndDeposit {
811                                    weight_limit: Weight::from_parts(10_000_000_000_000, 100_000_000),
812                                    deposit_limit: 100_000_000_000_000,
813                                })
814                                .unwrap(),
815                                &ExecConfig::new_substrate_tx_without_bump(),
816                            );
817                            match upload_result {
818                                Ok(upload_res) => {
819                                    let code_hash = upload_res.code_hash();
820                                    let contract_info = ContractInfo::<Runtime>::new(&account_h160, nonce as u32, code_hash.to_owned())
821                                        .expect("Failed to create contract info");
822                                    AccountInfo::<Runtime>::insert_contract(&account_h160, contract_info);
823                                }
824                                Err(err) => {
825                                    tracing::warn!(
826                                        address = ?address,
827                                        runtime_mode = ?ctx.runtime_mode,
828                                        bytecode_len = code_bytes.len(),
829                                        error = ?err,
830                                        "Failed to upload bytecode to pallet-revive, skipping migration"
831                                    );
832                                }
833                            }
834                        }
835                    }
836                    if AccountInfo::<Runtime>::load_contract(&account_h160).is_some() {
837                        migrate_contract_storage(data, address, account_h160);
838                    }
839                }
840            }
841        });
842}
843
844/// Migrates contract storage from REVM state to pallet-revive.
845///
846/// Merges storage from two sources:
847/// 1. Journaled state: most recent storage values from current execution
848/// 2. Database cache: storage written before startup migration, which is run as separate
849///    transaction, already committed to cache
850///
851/// The journaled state takes precedence - cache values are only used for slots
852/// not present in the journaled state.
853fn migrate_contract_storage(data: Ecx<'_, '_, '_>, address: Address, account_h160: H160) {
854    use std::collections::HashSet;
855
856    // Track which slots we've already migrated from the journaled state
857    let mut migrated_slots: HashSet<U256> = HashSet::new();
858
859    // First, migrate storage from the journaled state (most up-to-date values)
860    if let Some(account_state) = data.journaled_state.state.get(&address) {
861        for (slot, storage_slot) in &account_state.storage {
862            migrated_slots.insert(*slot);
863
864            let slot_bytes = slot.to_be_bytes::<32>();
865            let value = storage_slot.present_value;
866
867            if !value.is_zero() {
868                let _ = Pallet::<Runtime>::set_storage(
869                    account_h160,
870                    slot_bytes,
871                    Some(value.to_be_bytes::<32>().to_vec()),
872                );
873            } else {
874                // Handle case where storage was explicitly cleared
875                let _ = Pallet::<Runtime>::set_storage(account_h160, slot_bytes, None);
876            }
877        }
878    }
879
880    // Then, migrate storage from the database cache for slots NOT in journaled state
881    if let Some(cached_storage) = data.journaled_state.database.cached_storage(address) {
882        for (slot, value) in cached_storage {
883            // Skip slots already migrated from the journaled state
884            if migrated_slots.contains(&slot) {
885                continue;
886            }
887
888            let slot_bytes = slot.to_be_bytes::<32>();
889            if !value.is_zero() {
890                let _ = Pallet::<Runtime>::set_storage(
891                    account_h160,
892                    slot_bytes,
893                    Some(value.to_be_bytes::<32>().to_vec()),
894                );
895            } else {
896                // Handle case where storage was explicitly cleared
897                let _ = Pallet::<Runtime>::set_storage(account_h160, slot_bytes, None);
898            }
899        }
900    }
901}
902
903fn select_evm(ctx: &mut PvmCheatcodeInspectorStrategyContext, data: Ecx<'_, '_, '_>) {
904    if !ctx.using_revive {
905        tracing::info!("already using REVM");
906        return;
907    }
908
909    tracing::info!("switching from pallet-revive back to REVM");
910    ctx.using_revive = false;
911
912    ctx.externalities.execute_with(|| {
913        let block_number = System::block_number();
914        let timestamp = Timestamp::get();
915
916        data.block.number = U256::from(block_number);
917        data.block.timestamp = U256::from(timestamp / 1000);
918
919        let test_contract = data.journaled_state.database.get_test_contract_address();
920        let persistent_accounts = data.journaled_state.database.persistent_accounts().clone();
921        for address in persistent_accounts.into_iter().chain([data.tx.caller]) {
922            let account_evm = H160::from_slice(address.as_slice());
923            let pallet_evm_nonce = Pallet::<Runtime>::evm_nonce(&account_evm);
924            let pallet_evm_balance = Pallet::<Runtime>::evm_balance(&account_evm);
925            let amount_evm = U256::from_limbs(pallet_evm_balance.0);
926            let account = journaled_account(data, address).expect("failed to load account");
927            account.info.balance = amount_evm;
928            account.info.nonce = pallet_evm_nonce as u64;
929
930            // Migrate bytecode for deployed contracts (skip test contract)
931            if test_contract != Some(address)
932                && let Some(info) = AccountInfo::<Runtime>::load_contract(&account_evm)
933            {
934                let hash = hex::encode(info.code_hash);
935
936                // Try both PVM and EVM bytecode lookups since runtime_mode may not reflect
937                // the actual bytecode type stored (especially after backend switches)
938                // TODO: make PristineCode public to avoid this double lookup
939                let bytecode_result = ctx
940                    .dual_compiled_contracts
941                    .find_by_resolc_bytecode_hash(hash.clone())
942                    .and_then(|(_, contract)| {
943                        contract.evm_deployed_bytecode.as_bytes().map(|evm_bytecode| {
944                            (contract.evm_bytecode_hash, Bytecode::new_raw(evm_bytecode.clone()))
945                        })
946                    })
947                    .or_else(|| {
948                        ctx.dual_compiled_contracts.find_by_evm_bytecode_hash(hash).and_then(
949                            |(_, contract)| {
950                                contract.evm_deployed_bytecode.as_bytes().map(|evm_bytecode| {
951                                    (
952                                        contract.evm_bytecode_hash,
953                                        Bytecode::new_raw(evm_bytecode.clone()),
954                                    )
955                                })
956                            },
957                        )
958                    });
959
960                if let Some((code_hash, bytecode)) = bytecode_result {
961                    account.info.code_hash = code_hash;
962                    account.info.code = Some(bytecode);
963                } else {
964                    tracing::info!(
965                        address = ?address,
966                        "no EVM equivalent found for PVM bytecode, skipping migration"
967                    );
968                }
969            }
970        }
971    });
972}
973
974impl foundry_cheatcodes::CheatcodeInspectorStrategyExt for PvmCheatcodeInspectorStrategyRunner {
975    fn is_pvm_enabled(&self, state: &mut foundry_cheatcodes::Cheatcodes) -> bool {
976        let ctx = get_context_ref_mut(state.strategy.context.as_mut());
977
978        ctx.using_revive
979    }
980
981    /// Try handling the `CREATE` within PVM.
982    ///
983    /// If `Some` is returned then the result must be returned immediately, else the call must be
984    /// handled in EVM.
985    fn revive_try_create(
986        &self,
987        state: &mut foundry_cheatcodes::Cheatcodes,
988        ecx: Ecx<'_, '_, '_>,
989        input: &dyn CommonCreateInput,
990        executor: &mut dyn foundry_cheatcodes::CheatcodesExecutor,
991    ) -> Option<CreateOutcome> {
992        let mock_handler =
993            MockHandlerImpl::new(&ecx, &input.caller(), &ecx.tx.caller, None, None, state);
994
995        let ctx: &mut PvmCheatcodeInspectorStrategyContext =
996            get_context_ref_mut(state.strategy.context.as_mut());
997
998        if !ctx.using_revive {
999            return None;
1000        }
1001
1002        if ctx.skip_revive {
1003            ctx.skip_revive = false; // handled the skip, reset flag
1004            ctx.record_next_create_address = true;
1005            tracing::info!("running create in EVM, instead of pallet-revive (skipped)");
1006            return None;
1007        }
1008
1009        if let Some(CreateScheme::Create) = input.scheme() {
1010            let caller = input.caller();
1011            let nonce = ecx
1012                .journaled_state
1013                .load_account(input.caller())
1014                .expect("to load caller account")
1015                .info
1016                .nonce;
1017            let address = caller.create(nonce);
1018            if ecx
1019                .journaled_state
1020                .database
1021                .get_test_contract_address()
1022                .map(|addr| address == addr)
1023                .unwrap_or_default()
1024            {
1025                tracing::info!(
1026                    "running create in EVM, instead of PVM (Test Contract) {:#?}",
1027                    address
1028                );
1029                return None;
1030            }
1031        }
1032
1033        let init_code = input.init_code();
1034
1035        // Determine which bytecode to use based on runtime mode
1036        let (code_bytes, constructor_args) = match ctx.runtime_mode {
1037            crate::ReviveRuntimeMode::Pvm => {
1038                // PVM mode: use resolc (PVM) bytecode
1039                tracing::info!("running create in PVM mode with PVM bytecode");
1040                let find_contract = ctx
1041                    .dual_compiled_contracts
1042                    .find_bytecode(&init_code.0)
1043                    .unwrap_or_else(|| panic!("failed finding contract for {init_code:?}"));
1044                let constructor_args = find_contract.constructor_args();
1045                let contract = find_contract.contract();
1046                (contract.resolc_bytecode.as_bytes().unwrap().to_vec(), constructor_args.to_vec())
1047            }
1048            crate::ReviveRuntimeMode::Evm => {
1049                // EVM mode: use EVM bytecode directly (includes constructor args)
1050                tracing::info!("running create in EVM mode with EVM bytecode");
1051                (init_code.0.to_vec(), vec![])
1052            }
1053        };
1054
1055        let u128_max: U256 = U256::from(u128::MAX);
1056        if input.value() > u128_max {
1057            tracing::warn!(
1058                caller = ?input.caller(),
1059                value = ?input.value(),
1060                max = ?u128_max,
1061                "Create value exceeds u128::MAX. pallet-revive uses u128 for balances."
1062            );
1063            return Some(CreateOutcome {
1064                result: InterpreterResult {
1065                    result: InstructionResult::Revert,
1066                    output: Bytes::from_iter(
1067                        format!(
1068                            "Create value {} exceeds u128::MAX ({}). pallet-revive uses u128 for balances.",
1069                            input.value(),
1070                            u128::MAX
1071                        ).as_bytes(),
1072                    ),
1073                    gas: Gas::new(input.gas_limit()),
1074                },
1075                address: None,
1076            });
1077        }
1078
1079        let gas_price_pvm =
1080            sp_core::U256::from_little_endian(&U256::from(ecx.tx.gas_price).as_le_bytes());
1081        let mut tracer = Tracer::new(state.expected_calls.clone(), state.expected_creates.clone());
1082        let caller_h160 = H160::from_slice(input.caller().as_slice());
1083        let eth_deals = &state.eth_deals;
1084
1085        let res = ctx.externalities.execute_with(|| {
1086            tracer.watch_address(&caller_h160);
1087
1088            tracer.trace(|| {
1089                let exists = AccountInfo::<Runtime>::load_contract(&caller_h160).is_some();
1090                let origin_account_id =
1091                    AccountId32Mapper::<Runtime>::to_fallback_account_id(&caller_h160);
1092                let origin = OriginFor::<Runtime>::signed(origin_account_id.clone());
1093                let evm_value = sp_core::U256::from_little_endian(&input.value().as_le_bytes());
1094                MockHandlerImpl::fund_pranked_accounts(input.caller(), eth_deals);
1095                if !exists {
1096                    let nonce = ecx
1097                        .journaled_state
1098                        .load_account(input.caller())
1099                        .expect("to load caller account")
1100                        .info
1101                        .nonce;
1102                    polkadot_sdk::frame_system::Account::<Runtime>::mutate(
1103                        &origin_account_id,
1104                        |a| {
1105                            a.nonce =
1106                                nonce.min(u32::MAX.into()).try_into().expect("shouldn't happen");
1107                        },
1108                    );
1109                }
1110                System::inc_account_nonce(&origin_account_id);
1111                let code = Code::Upload(code_bytes.clone());
1112                let data = constructor_args;
1113                let salt = match input.scheme() {
1114                    Some(CreateScheme::Create2 { salt }) => Some(
1115                        salt.as_limbs()
1116                            .iter()
1117                            .flat_map(|&x| x.to_le_bytes())
1118                            .collect::<Vec<u8>>()
1119                            .try_into()
1120                            .unwrap(),
1121                    ),
1122                    _ => None,
1123                };
1124
1125                let exec_config = ExecConfig {
1126                    // IMPORTANT: Do NOT bump nonce here!
1127                    // When calling bare_instantiate directly (not through dispatch), the nonce
1128                    // has NOT been incremented pre-dispatch. Setting bump_nonce=true would cause
1129                    // pallet-revive to increment the nonce AFTER computing the CREATE address,
1130                    // but the address computation subtracts 1 from the nonce assuming it was
1131                    // already incremented. This causes all deployments to use nonce-1 for
1132                    // address computation, resulting in duplicate addresses.
1133                    bump_nonce: false,
1134                    collect_deposit_from_hold: None,
1135                    effective_gas_price: Some(gas_price_pvm),
1136                    mock_handler: Some(Box::new(mock_handler.clone())),
1137                    is_dry_run: None,
1138                };
1139
1140                Pallet::<Runtime>::bare_instantiate(
1141                    origin,
1142                    evm_value,
1143                    pallet_revive::TransactionLimits::WeightAndDeposit {
1144                        weight_limit: Weight::from_parts(10_000_000_000_000, 100_000_000),
1145                        deposit_limit: 100_000_000_000_000,
1146                    },
1147                    code,
1148                    data,
1149                    salt,
1150                    exec_config,
1151                )
1152            })
1153        });
1154        let mut gas = Gas::new(input.gas_limit());
1155
1156        post_exec(state, ecx, executor, &mut tracer, false);
1157        self.append_recorded_accesses(state, ecx, tracer.get_recorded_accesses());
1158
1159        mock_handler.update_state_mocks(state);
1160
1161        match &res.result {
1162            Ok(result) => {
1163                // Only record gas cost if gas metering is not paused.
1164                // When paused, the gas counter should remain frozen.
1165                if !state.gas_metering.paused {
1166                    let _ =
1167                        gas.record_cost(res.gas_consumed.min(u64::MAX.into()).try_into().unwrap());
1168                }
1169
1170                let outcome = if result.result.did_revert() {
1171                    CreateOutcome {
1172                        result: InterpreterResult {
1173                            result: InstructionResult::Revert,
1174                            output: result.result.data.clone().into(),
1175                            gas,
1176                        },
1177                        address: None,
1178                    }
1179                } else {
1180                    CreateOutcome {
1181                        result: InterpreterResult {
1182                            result: InstructionResult::Return,
1183                            output: code_bytes.into(),
1184                            gas,
1185                        },
1186                        address: Some(Address::from_slice(result.addr.as_bytes())),
1187                    }
1188                };
1189
1190                Some(outcome)
1191            }
1192            Err(e) => {
1193                tracing::error!("Contract creation failed: {e:#?}");
1194                Some(CreateOutcome {
1195                    result: InterpreterResult {
1196                        result: InstructionResult::Revert,
1197                        output: Bytes::from_iter(
1198                            format!("Contract creation failed: {e:#?}").as_bytes(),
1199                        ),
1200                        gas,
1201                    },
1202                    address: None,
1203                })
1204            }
1205        }
1206    }
1207
1208    /// Try handling the `CALL` within PVM.
1209    ///
1210    /// If `Some` is returned then the result must be returned immediately, else the call must be
1211    /// handled in EVM.
1212    fn revive_try_call(
1213        &self,
1214        state: &mut foundry_cheatcodes::Cheatcodes,
1215        ecx: Ecx<'_, '_, '_>,
1216        call: &CallInputs,
1217        executor: &mut dyn foundry_cheatcodes::CheatcodesExecutor,
1218    ) -> Option<CallOutcome> {
1219        let ctx = get_context_ref_mut(state.strategy.context.as_mut());
1220        let target_address = match call.scheme {
1221            CallScheme::DelegateCall => Some(call.target_address),
1222            _ => None,
1223        };
1224
1225        if !ctx.using_revive {
1226            return None;
1227        }
1228
1229        if ctx.skip_revive || ctx.skip_revive_addresses.contains(&call.target_address) {
1230            ctx.skip_revive = false; // handled the skip, reset flag
1231            tracing::info!("running call in EVM, instead of pallet-revive (skipped)");
1232            return None;
1233        }
1234
1235        if ecx
1236            .journaled_state
1237            .database
1238            .get_test_contract_address()
1239            .map(|addr| call.bytecode_address == addr || call.target_address == addr)
1240            .unwrap_or_default()
1241        {
1242            tracing::info!(
1243                "running call in EVM, instead of pallet-revive (Test Contract) {:#?}",
1244                call.bytecode_address
1245            );
1246            return None;
1247        }
1248
1249        tracing::info!("running call on pallet-revive with {} {:#?}", ctx.runtime_mode, call);
1250
1251        let u128_max: U256 = U256::from(u128::MAX);
1252        let call_value = call.call_value();
1253        if call_value > u128_max {
1254            tracing::warn!(
1255                caller = ?call.caller,
1256                target = ?call.target_address,
1257                value = ?call_value,
1258                max = ?u128_max,
1259                "Call value exceeds u128::MAX. pallet-revive uses u128 for balances."
1260            );
1261            return Some(CallOutcome {
1262                result: InterpreterResult {
1263                    result: InstructionResult::Revert,
1264                    output: Bytes::from_iter(
1265                        format!(
1266                            "Call value {} exceeds u128::MAX ({}). pallet-revive uses u128 for balances.",
1267                            call_value,
1268                            u128::MAX
1269                        ).as_bytes(),
1270                    ),
1271                    gas: Gas::new(call.gas_limit),
1272                },
1273                memory_offset: call.return_memory_offset.clone(),
1274            });
1275        }
1276
1277        let gas_price_pvm =
1278            sp_core::U256::from_little_endian(&U256::from(ecx.tx.gas_price).as_le_bytes());
1279        let mock_handler = MockHandlerImpl::new(
1280            &ecx,
1281            &call.caller,
1282            &ecx.tx.caller,
1283            target_address.as_ref(),
1284            Some(&call.bytecode_address),
1285            state,
1286        );
1287        let ctx = get_context_ref_mut(state.strategy.context.as_mut());
1288
1289        // Get nonce before execute_with closure
1290        let should_bump_nonce = !call.is_static;
1291        let caller_h160 = H160::from_slice(call.caller.as_slice());
1292
1293        let mut tracer = Tracer::new(state.expected_calls.clone(), state.expected_creates.clone());
1294        let eth_deals = &state.eth_deals;
1295        let res = ctx.externalities.execute_with(|| {
1296            // Watch the caller's address so its nonce changes get tracked in prestate trace
1297            tracer.watch_address(&caller_h160);
1298
1299            tracer.trace(|| {
1300                let origin = OriginFor::<Runtime>::signed(
1301                    AccountId32Mapper::<Runtime>::to_fallback_account_id(&caller_h160),
1302                );
1303                MockHandlerImpl::fund_pranked_accounts(call.caller, eth_deals);
1304
1305                let evm_value = sp_core::U256::from_little_endian(&call.call_value().as_le_bytes());
1306                let target = H160::from_slice(call.target_address.as_slice());
1307                let exec_config = ExecConfig {
1308                    bump_nonce: false, // only works for constructors
1309                    collect_deposit_from_hold: None,
1310                    effective_gas_price: Some(gas_price_pvm),
1311                    mock_handler: Some(Box::new(mock_handler.clone())),
1312                    is_dry_run: None,
1313                };
1314                if should_bump_nonce {
1315                    System::inc_account_nonce(
1316                        AccountId32Mapper::<Runtime>::to_fallback_account_id(&caller_h160),
1317                    );
1318                }
1319                Pallet::<Runtime>::bare_call(
1320                    origin,
1321                    target,
1322                    evm_value,
1323                    pallet_revive::TransactionLimits::WeightAndDeposit {
1324                        weight_limit: Weight::from_parts(10_000_000_000_000, 100_000_000),
1325                        deposit_limit: if call.is_static { 0 } else { 100_000_000_000_000 },
1326                    },
1327                    call.input.bytes(ecx).to_vec(),
1328                    exec_config,
1329                )
1330            })
1331        });
1332        mock_handler.update_state_mocks(state);
1333        let mut gas = Gas::new(call.gas_limit);
1334
1335        post_exec(state, ecx, executor, &mut tracer, call.is_static);
1336        self.append_recorded_accesses(state, ecx, tracer.get_recorded_accesses());
1337
1338        match res.result {
1339            Ok(result) => {
1340                // Only record gas cost if gas metering is not paused.
1341                // When paused, the gas counter should remain frozen.
1342                if !state.gas_metering.paused {
1343                    let _ =
1344                        gas.record_cost(res.gas_consumed.min(u64::MAX.into()).try_into().unwrap());
1345                }
1346
1347                let outcome = if result.did_revert() {
1348                    tracing::info!("Contract call reverted");
1349                    CallOutcome {
1350                        result: InterpreterResult {
1351                            result: InstructionResult::Revert,
1352                            output: result.data.into(),
1353                            gas,
1354                        },
1355                        memory_offset: call.return_memory_offset.clone(),
1356                    }
1357                } else if result.data.is_empty() {
1358                    CallOutcome {
1359                        result: InterpreterResult {
1360                            result: InstructionResult::Stop,
1361                            output: result.data.into(),
1362                            gas,
1363                        },
1364                        memory_offset: call.return_memory_offset.clone(),
1365                    }
1366                } else {
1367                    CallOutcome {
1368                        result: InterpreterResult {
1369                            result: InstructionResult::Return,
1370                            output: result.data.into(),
1371                            gas,
1372                        },
1373                        memory_offset: call.return_memory_offset.clone(),
1374                    }
1375                };
1376
1377                Some(outcome)
1378            }
1379            Err(e) => {
1380                tracing::info!("Contract call failed: {e:#?}");
1381                Some(CallOutcome {
1382                    result: InterpreterResult {
1383                        result: InstructionResult::Revert,
1384                        output: Bytes::from_iter(
1385                            format!("Contract call failed: {e:#?}").as_bytes(),
1386                        ),
1387                        gas,
1388                    },
1389                    memory_offset: call.return_memory_offset.clone(),
1390                })
1391            }
1392        }
1393    }
1394
1395    fn revive_remove_duplicate_account_access(&self, state: &mut foundry_cheatcodes::Cheatcodes) {
1396        let ctx = get_context_ref_mut(state.strategy.context.as_mut());
1397
1398        if let Some(index) = ctx.remove_recorded_access_at.take()
1399            && let Some(recorded_account_diffs_stack) = state.recorded_account_diffs_stack.as_mut()
1400            && let Some(last) = recorded_account_diffs_stack.last_mut()
1401        {
1402            // This entry has been inserted during CREATE/CALL operations in revm's
1403            // cheatcode inspector and must be removed.
1404            if index < last.len() {
1405                let removed = last.remove(index);
1406                if let Some(entry) = last.get_mut(index) {
1407                    entry.initialized = removed.initialized;
1408                }
1409            } else {
1410                warn!(index, len = last.len(), "skipping duplicate access removal: out of bounds");
1411            }
1412        }
1413    }
1414    fn revive_call_end(
1415        &self,
1416        state: &mut foundry_cheatcodes::Cheatcodes,
1417        ecx: Ecx<'_, '_, '_>,
1418        call: &CallInputs,
1419    ) {
1420        let ctx = get_context_ref_mut(state.strategy.context.as_mut());
1421
1422        // Skip storage sync if: in PVM mode AND no test contract
1423        if ctx.using_revive
1424            && ecx
1425                .journaled_state
1426                .database
1427                .get_test_contract_address()
1428                .map(|addr| call.bytecode_address != addr || call.target_address != addr)
1429                .unwrap_or(true)
1430        {
1431            return;
1432        }
1433
1434        apply_revm_storage_diff(ctx, ecx, call.target_address);
1435    }
1436
1437    fn revive_record_create_address(
1438        &self,
1439        state: &mut foundry_cheatcodes::Cheatcodes,
1440        outcome: &CreateOutcome,
1441    ) {
1442        let ctx = get_context_ref_mut(state.strategy.context.as_mut());
1443
1444        if ctx.record_next_create_address {
1445            ctx.record_next_create_address = false;
1446            if let Some(address) = outcome.address {
1447                ctx.skip_revive_addresses.insert(address);
1448                tracing::info!(
1449                    "recorded address {:?} for skip execution in the pallet-revive",
1450                    address
1451                );
1452            }
1453        }
1454    }
1455}
1456
1457fn post_exec(
1458    state: &mut foundry_cheatcodes::Cheatcodes,
1459    ecx: Ecx<'_, '_, '_>,
1460    executor: &mut dyn foundry_cheatcodes::CheatcodesExecutor,
1461    tracer: &mut Tracer,
1462    is_static_call: bool,
1463) {
1464    let ctx = &mut get_context_ref_mut(state.strategy.context.as_mut());
1465
1466    let externalities = &mut ctx.externalities;
1467    let dual_compiled_contracts = &ctx.dual_compiled_contracts;
1468    let (call_traces, create_traces) = externalities.execute_with(|| {
1469        tracer.apply_prestate_trace(ecx);
1470        (tracer.collect_call_traces(), tracer.create_tracer.finalize(dual_compiled_contracts))
1471    });
1472    if let Some(traces) = call_traces
1473        && !is_static_call
1474    {
1475        let mut logs: Vec<(u32, Log)> = vec![];
1476        logs.sort_by(|a, b| a.0.cmp(&b.0));
1477        if !state.expected_emits.is_empty() || state.recorded_logs.is_some() {
1478            logs = collect_logs(&traces);
1479        }
1480        if !state.expected_emits.is_empty() {
1481            logs.clone().into_iter().for_each(|(_, log)| {
1482                foundry_cheatcodes::handle_expect_emit(state, &log, &mut Default::default());
1483            })
1484        }
1485        if let Some(records) = &mut state.recorded_logs {
1486            records.extend(logs.iter().map(|(_, log)| foundry_cheatcodes::Vm::Log {
1487                data: log.data.data.clone(),
1488                emitter: log.address,
1489                topics: log.topics().to_owned(),
1490            }));
1491        };
1492        executor.trace_revive(state, ecx, Box::new(traces));
1493    }
1494
1495    if let Some(expected_revert) = &mut state.expected_revert {
1496        expected_revert.max_depth = std::cmp::max(
1497            ecx.journaled_state.depth() + tracer.revert_tracer.max_depth,
1498            expected_revert.max_depth,
1499        );
1500        expected_revert.reverted_by = tracer.revert_tracer.has_reverted.map(|x| Address::from(x.0));
1501    }
1502    state.expected_calls = tracer.expect_call_tracer.data.clone();
1503    state.expected_creates = create_traces;
1504}
1505
1506struct LogWithIndex {
1507    log: CallTrace,
1508    index: Vec<(Log, u32)>,
1509}
1510
1511impl From<CallTrace> for LogWithIndex {
1512    fn from(value: CallTrace) -> Self {
1513        Self { log: value, index: vec![] }
1514    }
1515}
1516
1517fn assign_indexes(trace: &mut LogWithIndex, mut index: u32) -> (u32, Vec<(Log, u32)>) {
1518    let mut sub_call_index = 0;
1519    for (i, _) in trace.log.logs.clone().iter().enumerate() {
1520        while sub_call_index < trace.log.logs[i].position {
1521            let (new_index, logs) =
1522                assign_indexes(&mut trace.log.calls[sub_call_index as usize].clone().into(), index);
1523            index = new_index;
1524            trace.index.extend(logs.into_iter());
1525            sub_call_index += 1;
1526        }
1527        let log = trace.log.logs[i].clone();
1528        trace.index.push((
1529            Log::new_unchecked(
1530                Address::from(log.address.0),
1531                log.topics.iter().map(|x| U256::from_be_slice(x.as_bytes()).into()).collect(),
1532                Bytes::from(log.data.0),
1533            ),
1534            index,
1535        ));
1536        index += 1;
1537    }
1538    while (sub_call_index as usize) < trace.log.calls.len() {
1539        let (new_index, logs) =
1540            assign_indexes(&mut trace.log.calls[sub_call_index as usize].clone().into(), index);
1541        index = new_index;
1542        trace.index.extend(logs.into_iter());
1543        sub_call_index += 1;
1544    }
1545    (index, trace.index.clone())
1546}
1547
1548fn collect_logs(trace: &CallTrace) -> Vec<(u32, Log)> {
1549    let (_, mut l) = assign_indexes(&mut trace.clone().into(), 0);
1550    l.sort_by(|a, b| a.1.cmp(&b.1));
1551    l.into_iter().map(|x| (x.1, x.0)).collect()
1552}
1553
1554fn get_context_ref_mut(
1555    ctx: &mut dyn CheatcodeInspectorStrategyContext,
1556) -> &mut PvmCheatcodeInspectorStrategyContext {
1557    ctx.as_any_mut().downcast_mut().expect("expected PvmCheatcodeInspectorStrategyContext")
1558}
1559
1560fn apply_revm_storage_diff(
1561    ctx: &mut PvmCheatcodeInspectorStrategyContext,
1562    ecx: Ecx<'_, '_, '_>,
1563    address: Address,
1564) {
1565    let Some(account_state) = ecx.journaled_state.state.get(&address) else {
1566        return;
1567    };
1568
1569    let h160_address = H160::from_slice(address.as_slice());
1570
1571    // Check if contract exists in pallet-revive before applying storage diffs
1572    let contract_exists = ctx
1573        .externalities
1574        .execute_with(|| AccountInfo::<Runtime>::load_contract(&h160_address).is_some());
1575
1576    if !contract_exists {
1577        return;
1578    }
1579
1580    ctx.externalities.execute_with(|| {
1581        for (slot, storage_slot) in &account_state.storage {
1582            if storage_slot.is_changed() {
1583                let slot_bytes = slot.to_be_bytes::<32>();
1584                let new_value = storage_slot.present_value;
1585
1586                if !new_value.is_zero() {
1587                    let _ = Pallet::<Runtime>::set_storage(
1588                        h160_address,
1589                        slot_bytes,
1590                        Some(new_value.to_be_bytes::<32>().to_vec()),
1591                    );
1592                } else {
1593                    let _ = Pallet::<Runtime>::set_storage(h160_address, slot_bytes, None);
1594                }
1595            }
1596        }
1597    });
1598}