diff --git a/.github/workflows/ci-core-reusable.yml b/.github/workflows/ci-core-reusable.yml
index 9aaa476d740d..6bb48b0974e5 100644
--- a/.github/workflows/ci-core-reusable.yml
+++ b/.github/workflows/ci-core-reusable.yml
@@ -101,7 +101,7 @@ jobs:
 
       - name: Loadtest configuration
         run: |
-          echo EXPECTED_TX_COUNT=${{ matrix.vm_mode == 'NEW' && 21000 || 16000 }} >> .env
+          echo EXPECTED_TX_COUNT=${{ matrix.vm_mode == 'NEW' && 30000 || 16000 }} >> .env
           echo ACCOUNTS_AMOUNT="100" >> .env
           echo MAX_INFLIGHT_TXS="10" >> .env
           echo SYNC_API_REQUESTS_LIMIT="15" >> .env
@@ -356,12 +356,16 @@ jobs:
 
       - name: Run servers
         run: |
+          # Override config for part of chains to test the default config as well
+          ci_run zkstack dev config-writer --path etc/env/file_based/overrides/tests/integration.yaml --chain era
+          ci_run zkstack dev config-writer --path etc/env/file_based/overrides/tests/integration.yaml --chain validium
+          
           ci_run zkstack server --ignore-prerequisites --chain era &> ${{ env.SERVER_LOGS_DIR }}/rollup.log &
           ci_run zkstack server --ignore-prerequisites --chain validium &> ${{ env.SERVER_LOGS_DIR }}/validium.log &
           ci_run zkstack server --ignore-prerequisites --chain custom_token &> ${{ env.SERVER_LOGS_DIR }}/custom_token.log &
           ci_run zkstack server --ignore-prerequisites --chain consensus \
-          --components=api,tree,eth,state_keeper,housekeeper,commitment_generator,vm_runner_protective_reads,vm_runner_bwip,vm_playground,da_dispatcher,consensus \
-          &> ${{ env.SERVER_LOGS_DIR }}/consensus.log &
+            --components=api,tree,eth,state_keeper,housekeeper,commitment_generator,vm_runner_protective_reads,vm_runner_bwip,vm_playground,da_dispatcher,consensus \
+            &> ${{ env.SERVER_LOGS_DIR }}/consensus.log &
 
           ci_run sleep 5
 
diff --git a/Cargo.lock b/Cargo.lock
index 64ae0a9a12f4..de2c2d6c9b22 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -11351,6 +11351,7 @@ dependencies = [
  "assert_matches",
  "async-trait",
  "once_cell",
+ "test-casing",
  "tokio",
  "tracing",
  "vise",
diff --git a/core/bin/zksync_server/src/node_builder.rs b/core/bin/zksync_server/src/node_builder.rs
index e2bd487f22b6..19edef6e4eec 100644
--- a/core/bin/zksync_server/src/node_builder.rs
+++ b/core/bin/zksync_server/src/node_builder.rs
@@ -309,10 +309,12 @@ impl MainNodeBuilder {
             latest_values_cache_size: rpc_config.latest_values_cache_size() as u64,
             latest_values_max_block_lag: rpc_config.latest_values_max_block_lag(),
         };
+        let vm_config = try_load_config!(self.configs.experimental_vm_config);
 
         // On main node we always use master pool sink.
         self.node.add_layer(MasterPoolSinkLayer);
-        self.node.add_layer(TxSenderLayer::new(
+
+        let layer = TxSenderLayer::new(
             TxSenderConfig::new(
                 &sk_config,
                 &rpc_config,
@@ -323,7 +325,9 @@ impl MainNodeBuilder {
             ),
             postgres_storage_caches_config,
             rpc_config.vm_concurrency_limit(),
-        ));
+        );
+        let layer = layer.with_vm_mode(vm_config.api_fast_vm_mode);
+        self.node.add_layer(layer);
         Ok(self)
     }
 
diff --git a/core/lib/config/src/configs/experimental.rs b/core/lib/config/src/configs/experimental.rs
index 618cfd3d388c..a87a221ef222 100644
--- a/core/lib/config/src/configs/experimental.rs
+++ b/core/lib/config/src/configs/experimental.rs
@@ -106,4 +106,9 @@ pub struct ExperimentalVmConfig {
     /// the new VM doesn't produce call traces and can diverge from the old VM!
     #[serde(default)]
     pub state_keeper_fast_vm_mode: FastVmMode,
+
+    /// Fast VM mode to use in the API server. Currently, some operations are not supported by the fast VM (e.g., `debug_traceCall`
+    /// or transaction validation), so the legacy VM will always be used for them.
+    #[serde(default)]
+    pub api_fast_vm_mode: FastVmMode,
 }
diff --git a/core/lib/config/src/testonly.rs b/core/lib/config/src/testonly.rs
index ce681cc0cc43..2c4d91a2f358 100644
--- a/core/lib/config/src/testonly.rs
+++ b/core/lib/config/src/testonly.rs
@@ -336,6 +336,7 @@ impl Distribution<configs::ExperimentalVmConfig> for EncodeDist {
         configs::ExperimentalVmConfig {
             playground: self.sample(rng),
             state_keeper_fast_vm_mode: gen_fast_vm_mode(rng),
+            api_fast_vm_mode: gen_fast_vm_mode(rng),
         }
     }
 }
diff --git a/core/lib/env_config/src/vm_runner.rs b/core/lib/env_config/src/vm_runner.rs
index 730a79dd340a..0a29d1256bd2 100644
--- a/core/lib/env_config/src/vm_runner.rs
+++ b/core/lib/env_config/src/vm_runner.rs
@@ -55,6 +55,7 @@ mod tests {
         let mut lock = MUTEX.lock();
         let config = r#"
             EXPERIMENTAL_VM_STATE_KEEPER_FAST_VM_MODE=new
+            EXPERIMENTAL_VM_API_FAST_VM_MODE=shadow
             EXPERIMENTAL_VM_PLAYGROUND_FAST_VM_MODE=shadow
             EXPERIMENTAL_VM_PLAYGROUND_DB_PATH=/db/vm_playground
             EXPERIMENTAL_VM_PLAYGROUND_FIRST_PROCESSED_BATCH=123
@@ -64,6 +65,7 @@ mod tests {
 
         let config = ExperimentalVmConfig::from_env().unwrap();
         assert_eq!(config.state_keeper_fast_vm_mode, FastVmMode::New);
+        assert_eq!(config.api_fast_vm_mode, FastVmMode::Shadow);
         assert_eq!(config.playground.fast_vm_mode, FastVmMode::Shadow);
         assert_eq!(config.playground.db_path.unwrap(), "/db/vm_playground");
         assert_eq!(config.playground.first_processed_batch, L1BatchNumber(123));
diff --git a/core/lib/multivm/src/versions/testonly/l1_tx_execution.rs b/core/lib/multivm/src/versions/testonly/l1_tx_execution.rs
index e98a8385f020..37a2bf2bec20 100644
--- a/core/lib/multivm/src/versions/testonly/l1_tx_execution.rs
+++ b/core/lib/multivm/src/versions/testonly/l1_tx_execution.rs
@@ -1,3 +1,4 @@
+use assert_matches::assert_matches;
 use ethabi::Token;
 use zksync_contracts::l1_messenger_contract;
 use zksync_system_constants::{BOOTLOADER_ADDRESS, L1_MESSENGER_ADDRESS};
@@ -5,13 +6,17 @@ use zksync_test_account::TxType;
 use zksync_types::{
     get_code_key, get_known_code_key,
     l2_to_l1_log::{L2ToL1Log, UserL2ToL1Log},
-    Execute, ExecuteTransactionCommon, U256,
+    Address, Execute, ExecuteTransactionCommon, U256,
 };
 use zksync_utils::{h256_to_u256, u256_to_h256};
 
-use super::{read_test_contract, tester::VmTesterBuilder, TestedVm, BASE_SYSTEM_CONTRACTS};
+use super::{
+    read_test_contract, tester::VmTesterBuilder, ContractToDeploy, TestedVm, BASE_SYSTEM_CONTRACTS,
+};
 use crate::{
-    interface::{InspectExecutionMode, TxExecutionMode, VmInterfaceExt},
+    interface::{
+        ExecutionResult, InspectExecutionMode, TxExecutionMode, VmInterfaceExt, VmRevertReason,
+    },
     utils::StorageWritesDeduplicator,
 };
 
@@ -180,3 +185,41 @@ pub(crate) fn test_l1_tx_execution_high_gas_limit<VM: TestedVm>() {
 
     assert!(res.result.is_failed(), "The transaction should've failed");
 }
+
+pub(crate) fn test_l1_tx_execution_gas_estimation_with_low_gas<VM: TestedVm>() {
+    let counter_contract = read_test_contract();
+    let counter_address = Address::repeat_byte(0x11);
+    let mut vm = VmTesterBuilder::new()
+        .with_empty_in_memory_storage()
+        .with_base_system_smart_contracts(BASE_SYSTEM_CONTRACTS.clone())
+        .with_execution_mode(TxExecutionMode::EstimateFee)
+        .with_custom_contracts(vec![ContractToDeploy::new(
+            counter_contract,
+            counter_address,
+        )])
+        .with_rich_accounts(1)
+        .build::<VM>();
+
+    let account = &mut vm.rich_accounts[0];
+    let mut tx = account.get_test_contract_transaction(
+        counter_address,
+        false,
+        None,
+        false,
+        TxType::L1 { serial_id: 0 },
+    );
+    let ExecuteTransactionCommon::L1(data) = &mut tx.common_data else {
+        unreachable!();
+    };
+    // This gas limit is chosen so that transaction starts getting executed by the bootloader, but then runs out of gas
+    // before its execution result is posted.
+    data.gas_limit = 15_000.into();
+
+    vm.vm.push_transaction(tx);
+    let res = vm.vm.execute(InspectExecutionMode::OneTx);
+    assert_matches!(
+        &res.result,
+        ExecutionResult::Revert { output: VmRevertReason::General { msg, .. } }
+            if msg.contains("reverted with empty reason")
+    );
+}
diff --git a/core/lib/multivm/src/versions/vm_fast/tests/l1_tx_execution.rs b/core/lib/multivm/src/versions/vm_fast/tests/l1_tx_execution.rs
index 0174eeffd7e3..f02957020178 100644
--- a/core/lib/multivm/src/versions/vm_fast/tests/l1_tx_execution.rs
+++ b/core/lib/multivm/src/versions/vm_fast/tests/l1_tx_execution.rs
@@ -1,6 +1,7 @@
 use crate::{
     versions::testonly::l1_tx_execution::{
-        test_l1_tx_execution, test_l1_tx_execution_high_gas_limit,
+        test_l1_tx_execution, test_l1_tx_execution_gas_estimation_with_low_gas,
+        test_l1_tx_execution_high_gas_limit,
     },
     vm_fast::Vm,
 };
@@ -14,3 +15,8 @@ fn l1_tx_execution() {
 fn l1_tx_execution_high_gas_limit() {
     test_l1_tx_execution_high_gas_limit::<Vm<_>>();
 }
+
+#[test]
+fn l1_tx_execution_gas_estimation_with_low_gas() {
+    test_l1_tx_execution_gas_estimation_with_low_gas::<Vm<_>>();
+}
diff --git a/core/lib/multivm/src/versions/vm_fast/vm.rs b/core/lib/multivm/src/versions/vm_fast/vm.rs
index a2114a339481..6ebc4b9c5716 100644
--- a/core/lib/multivm/src/versions/vm_fast/vm.rs
+++ b/core/lib/multivm/src/versions/vm_fast/vm.rs
@@ -51,8 +51,8 @@ use crate::{
     },
     vm_latest::{
         constants::{
-            get_vm_hook_params_start_position, get_vm_hook_position, OPERATOR_REFUNDS_OFFSET,
-            TX_GAS_LIMIT_OFFSET, VM_HOOK_PARAMS_COUNT,
+            get_result_success_first_slot, get_vm_hook_params_start_position, get_vm_hook_position,
+            OPERATOR_REFUNDS_OFFSET, TX_GAS_LIMIT_OFFSET, VM_HOOK_PARAMS_COUNT,
         },
         MultiVMSubversion,
     },
@@ -213,7 +213,22 @@ impl<S: ReadStorage, Tr: Tracer + Default> Vm<S, Tr> {
                 }
                 Hook::TxHasEnded => {
                     if let VmExecutionMode::OneTx = execution_mode {
-                        break (last_tx_result.take().unwrap(), false);
+                        // The bootloader may invoke `TxHasEnded` hook without posting a tx result previously. One case when this can happen
+                        // is estimating gas for L1 transactions, if a transaction runs out of gas during execution.
+                        let tx_result = last_tx_result.take().unwrap_or_else(|| {
+                            let tx_has_failed = self.get_tx_result().is_zero();
+                            if tx_has_failed {
+                                let output = VmRevertReason::General {
+                                    msg: "Transaction reverted with empty reason. Possibly out of gas"
+                                        .to_string(),
+                                    data: vec![],
+                                };
+                                ExecutionResult::Revert { output }
+                            } else {
+                                ExecutionResult::Success { output: vec![] }
+                            }
+                        });
+                        break (tx_result, false);
                     }
                 }
                 Hook::AskOperatorForRefund => {
@@ -361,6 +376,12 @@ impl<S: ReadStorage, Tr: Tracer + Default> Vm<S, Tr> {
             .unwrap()
     }
 
+    fn get_tx_result(&self) -> U256 {
+        let tx_idx = self.bootloader_state.current_tx();
+        let slot = get_result_success_first_slot(VM_VERSION) as usize + tx_idx;
+        self.read_word_from_bootloader_heap(slot)
+    }
+
     fn get_debug_log(&self) -> (String, String) {
         let hook_params = self.get_hook_params();
         let mut msg = u256_to_h256(hook_params[0]).as_bytes().to_vec();
diff --git a/core/lib/multivm/src/versions/vm_latest/tests/l1_tx_execution.rs b/core/lib/multivm/src/versions/vm_latest/tests/l1_tx_execution.rs
index 4b7429c28296..3b8a01dbc80f 100644
--- a/core/lib/multivm/src/versions/vm_latest/tests/l1_tx_execution.rs
+++ b/core/lib/multivm/src/versions/vm_latest/tests/l1_tx_execution.rs
@@ -1,6 +1,7 @@
 use crate::{
     versions::testonly::l1_tx_execution::{
-        test_l1_tx_execution, test_l1_tx_execution_high_gas_limit,
+        test_l1_tx_execution, test_l1_tx_execution_gas_estimation_with_low_gas,
+        test_l1_tx_execution_high_gas_limit,
     },
     vm_latest::{HistoryEnabled, Vm},
 };
@@ -14,3 +15,8 @@ fn l1_tx_execution() {
 fn l1_tx_execution_high_gas_limit() {
     test_l1_tx_execution_high_gas_limit::<Vm<_, HistoryEnabled>>();
 }
+
+#[test]
+fn l1_tx_execution_gas_estimation_with_low_gas() {
+    test_l1_tx_execution_gas_estimation_with_low_gas::<Vm<_, HistoryEnabled>>();
+}
diff --git a/core/lib/multivm/src/vm_instance.rs b/core/lib/multivm/src/vm_instance.rs
index 5ff27046377a..e2f72bd24113 100644
--- a/core/lib/multivm/src/vm_instance.rs
+++ b/core/lib/multivm/src/vm_instance.rs
@@ -234,7 +234,7 @@ pub type ShadowedFastVm<S, Tr = ()> = ShadowVm<
 
 /// Fast VM variants.
 #[derive(Debug)]
-pub enum FastVmInstance<S: ReadStorage, Tr> {
+pub enum FastVmInstance<S: ReadStorage, Tr = ()> {
     /// Fast VM running in isolation.
     Fast(crate::vm_fast::Vm<ImmutableStorageView<S>, Tr>),
     /// Fast VM shadowed by the latest legacy VM.
diff --git a/core/lib/protobuf_config/src/experimental.rs b/core/lib/protobuf_config/src/experimental.rs
index 63fa0ca51eb5..750dc7b04f01 100644
--- a/core/lib/protobuf_config/src/experimental.rs
+++ b/core/lib/protobuf_config/src/experimental.rs
@@ -7,6 +7,14 @@ use zksync_protobuf::{repr::ProtoRepr, required};
 
 use crate::{proto::experimental as proto, read_optional_repr};
 
+fn parse_vm_mode(raw: Option<i32>) -> anyhow::Result<FastVmMode> {
+    Ok(raw
+        .map(proto::FastVmMode::try_from)
+        .transpose()
+        .context("fast_vm_mode")?
+        .map_or_else(FastVmMode::default, |mode| mode.parse()))
+}
+
 impl ProtoRepr for proto::Db {
     type Type = configs::ExperimentalDBConfig;
 
@@ -105,12 +113,8 @@ impl ProtoRepr for proto::Vm {
     fn read(&self) -> anyhow::Result<Self::Type> {
         Ok(Self::Type {
             playground: read_optional_repr(&self.playground).unwrap_or_default(),
-            state_keeper_fast_vm_mode: self
-                .state_keeper_fast_vm_mode
-                .map(proto::FastVmMode::try_from)
-                .transpose()
-                .context("fast_vm_mode")?
-                .map_or_else(FastVmMode::default, |mode| mode.parse()),
+            state_keeper_fast_vm_mode: parse_vm_mode(self.state_keeper_fast_vm_mode)?,
+            api_fast_vm_mode: parse_vm_mode(self.api_fast_vm_mode)?,
         })
     }
 
@@ -120,6 +124,7 @@ impl ProtoRepr for proto::Vm {
             state_keeper_fast_vm_mode: Some(
                 proto::FastVmMode::new(this.state_keeper_fast_vm_mode).into(),
             ),
+            api_fast_vm_mode: Some(proto::FastVmMode::new(this.api_fast_vm_mode).into()),
         }
     }
 }
diff --git a/core/lib/protobuf_config/src/proto/config/experimental.proto b/core/lib/protobuf_config/src/proto/config/experimental.proto
index 5e1d045ca670..87af8d3835c6 100644
--- a/core/lib/protobuf_config/src/proto/config/experimental.proto
+++ b/core/lib/protobuf_config/src/proto/config/experimental.proto
@@ -37,4 +37,5 @@ message VmPlayground {
 message Vm {
   optional VmPlayground playground = 1; // optional
   optional FastVmMode state_keeper_fast_vm_mode = 2; // optional; if not set, fast VM is not used
+  optional FastVmMode api_fast_vm_mode = 3; // optional; if not set, fast VM is not used
 }
diff --git a/core/lib/vm_executor/Cargo.toml b/core/lib/vm_executor/Cargo.toml
index a967aaa969ad..06a531252c54 100644
--- a/core/lib/vm_executor/Cargo.toml
+++ b/core/lib/vm_executor/Cargo.toml
@@ -26,3 +26,4 @@ vise.workspace = true
 
 [dev-dependencies]
 assert_matches.workspace = true
+test-casing.workspace = true
diff --git a/core/lib/vm_executor/src/oneshot/metrics.rs b/core/lib/vm_executor/src/oneshot/metrics.rs
index 475463300f16..13a832ee3c89 100644
--- a/core/lib/vm_executor/src/oneshot/metrics.rs
+++ b/core/lib/vm_executor/src/oneshot/metrics.rs
@@ -50,7 +50,7 @@ pub(super) fn report_vm_memory_metrics(
     tx_id: &str,
     memory_metrics: &VmMemoryMetrics,
     vm_execution_took: Duration,
-    storage_metrics: &StorageViewStats,
+    storage_stats: &StorageViewStats,
 ) {
     MEMORY_METRICS.event_sink_size[&SizeType::Inner].observe(memory_metrics.event_sink_inner);
     MEMORY_METRICS.event_sink_size[&SizeType::History].observe(memory_metrics.event_sink_history);
@@ -65,10 +65,18 @@ pub(super) fn report_vm_memory_metrics(
 
     MEMORY_METRICS
         .storage_view_cache_size
-        .observe(storage_metrics.cache_size);
+        .observe(storage_stats.cache_size);
     MEMORY_METRICS
         .full
-        .observe(memory_metrics.full_size() + storage_metrics.cache_size);
+        .observe(memory_metrics.full_size() + storage_stats.cache_size);
 
-    STORAGE_METRICS.observe(&format!("Tx {tx_id}"), vm_execution_took, storage_metrics);
+    report_vm_storage_metrics(tx_id, vm_execution_took, storage_stats);
+}
+
+pub(super) fn report_vm_storage_metrics(
+    tx_id: &str,
+    vm_execution_took: Duration,
+    storage_stats: &StorageViewStats,
+) {
+    STORAGE_METRICS.observe(&format!("Tx {tx_id}"), vm_execution_took, storage_stats);
 }
diff --git a/core/lib/vm_executor/src/oneshot/mod.rs b/core/lib/vm_executor/src/oneshot/mod.rs
index 5f9e4dd3c6f4..154c838f824f 100644
--- a/core/lib/vm_executor/src/oneshot/mod.rs
+++ b/core/lib/vm_executor/src/oneshot/mod.rs
@@ -17,23 +17,26 @@ use once_cell::sync::OnceCell;
 use zksync_multivm::{
     interface::{
         executor::{OneshotExecutor, TransactionValidator},
-        storage::{ReadStorage, StoragePtr, StorageView, WriteStorage},
+        storage::{ReadStorage, StorageView, StorageWithOverrides},
         tracer::{ValidationError, ValidationParams},
-        ExecutionResult, InspectExecutionMode, OneshotEnv, OneshotTracingParams,
+        utils::{DivergenceHandler, ShadowVm},
+        Call, ExecutionResult, InspectExecutionMode, OneshotEnv, OneshotTracingParams,
         OneshotTransactionExecutionResult, StoredL2BlockEnv, TxExecutionArgs, TxExecutionMode,
-        VmInterface,
+        VmFactory, VmInterface,
     },
-    tracers::{CallTracer, StorageInvocations, ValidationTracer},
+    is_supported_by_fast_vm,
+    tracers::{CallTracer, StorageInvocations, TracerDispatcher, ValidationTracer},
     utils::adjust_pubdata_price_for_tx,
-    vm_latest::HistoryDisabled,
+    vm_latest::{HistoryDisabled, HistoryEnabled},
     zk_evm_latest::ethereum_types::U256,
-    LegacyVmInstance, MultiVMTracer,
+    FastVmInstance, HistoryMode, LegacyVmInstance, MultiVMTracer,
 };
 use zksync_types::{
     block::pack_block_info,
     get_nonce_key,
     l2::L2Tx,
     utils::{decompose_full_nonce, nonces_to_full_nonce, storage_key_for_eth_balance},
+    vm::FastVmMode,
     AccountTreeId, Nonce, StorageKey, Transaction, SYSTEM_CONTEXT_ADDRESS,
     SYSTEM_CONTEXT_CURRENT_L2_BLOCK_INFO_POSITION, SYSTEM_CONTEXT_CURRENT_TX_ROLLING_HASH_POSITION,
 };
@@ -54,10 +57,14 @@ mod contracts;
 mod env;
 mod metrics;
 mod mock;
+#[cfg(test)]
+mod tests;
 
 /// Main [`OneshotExecutor`] implementation used by the API server.
-#[derive(Debug, Default)]
+#[derive(Debug)]
 pub struct MainOneshotExecutor {
+    fast_vm_mode: FastVmMode,
+    panic_on_divergence: bool,
     missed_storage_invocation_limit: usize,
     execution_latency_histogram: Option<&'static vise::Histogram<Duration>>,
 }
@@ -67,11 +74,28 @@ impl MainOneshotExecutor {
     /// The limit is applied for calls and gas estimations, but not during transaction validation.
     pub fn new(missed_storage_invocation_limit: usize) -> Self {
         Self {
+            fast_vm_mode: FastVmMode::Old,
+            panic_on_divergence: false,
             missed_storage_invocation_limit,
             execution_latency_histogram: None,
         }
     }
 
+    /// Sets the fast VM mode used by this executor.
+    pub fn set_fast_vm_mode(&mut self, fast_vm_mode: FastVmMode) {
+        if !matches!(fast_vm_mode, FastVmMode::Old) {
+            tracing::warn!(
+                "Running new VM with modes {fast_vm_mode:?}; this can lead to incorrect node behavior"
+            );
+        }
+        self.fast_vm_mode = fast_vm_mode;
+    }
+
+    /// Causes the VM to panic on divergence whenever it executes in the shadow mode. By default, a divergence is logged on `ERROR` level.
+    pub fn panic_on_divergence(&mut self) {
+        self.panic_on_divergence = true;
+    }
+
     /// Sets a histogram for measuring VM execution latency.
     pub fn set_execution_latency_histogram(
         &mut self,
@@ -79,19 +103,31 @@ impl MainOneshotExecutor {
     ) {
         self.execution_latency_histogram = Some(histogram);
     }
+
+    fn select_fast_vm_mode(
+        &self,
+        env: &OneshotEnv,
+        tracing_params: &OneshotTracingParams,
+    ) -> FastVmMode {
+        if tracing_params.trace_calls || !is_supported_by_fast_vm(env.system.version) {
+            FastVmMode::Old // the fast VM doesn't support call tracing or old protocol versions
+        } else {
+            self.fast_vm_mode
+        }
+    }
 }
 
 #[async_trait]
-impl<S> OneshotExecutor<S> for MainOneshotExecutor
+impl<S> OneshotExecutor<StorageWithOverrides<S>> for MainOneshotExecutor
 where
     S: ReadStorage + Send + 'static,
 {
     async fn inspect_transaction_with_bytecode_compression(
         &self,
-        storage: S,
+        storage: StorageWithOverrides<S>,
         env: OneshotEnv,
         args: TxExecutionArgs,
-        params: OneshotTracingParams,
+        tracing_params: OneshotTracingParams,
     ) -> anyhow::Result<OneshotTransactionExecutionResult> {
         let missed_storage_invocation_limit = match env.system.execution_mode {
             // storage accesses are not limited for tx validation
@@ -100,35 +136,24 @@ where
                 self.missed_storage_invocation_limit
             }
         };
-        let execution_latency_histogram = self.execution_latency_histogram;
+        let sandbox = VmSandbox {
+            fast_vm_mode: self.select_fast_vm_mode(&env, &tracing_params),
+            panic_on_divergence: self.panic_on_divergence,
+            storage,
+            env,
+            execution_args: args,
+            execution_latency_histogram: self.execution_latency_histogram,
+        };
 
         tokio::task::spawn_blocking(move || {
-            let mut tracers = vec![];
-            let mut calls_result = Arc::<OnceCell<_>>::default();
-            if params.trace_calls {
-                tracers.push(CallTracer::new(calls_result.clone()).into_tracer_pointer());
-            }
-            tracers.push(
-                StorageInvocations::new(missed_storage_invocation_limit).into_tracer_pointer(),
-            );
-
-            let executor = VmSandbox::new(storage, env, args, execution_latency_histogram);
-            let mut result = executor.apply(|vm, transaction| {
-                let (compression_result, tx_result) = vm
-                    .inspect_transaction_with_bytecode_compression(
-                        &mut tracers.into(),
-                        transaction,
-                        true,
-                    );
-                OneshotTransactionExecutionResult {
-                    tx_result: Box::new(tx_result),
-                    compression_result: compression_result.map(drop),
-                    call_traces: vec![],
-                }
-            });
-
-            result.call_traces = Arc::make_mut(&mut calls_result).take().unwrap_or_default();
-            result
+            sandbox.execute_in_vm(|vm, transaction| {
+                vm.inspect_transaction_with_bytecode_compression(
+                    missed_storage_invocation_limit,
+                    tracing_params,
+                    transaction,
+                    true,
+                )
+            })
         })
         .await
         .context("VM execution panicked")
@@ -136,13 +161,13 @@ where
 }
 
 #[async_trait]
-impl<S> TransactionValidator<S> for MainOneshotExecutor
+impl<S> TransactionValidator<StorageWithOverrides<S>> for MainOneshotExecutor
 where
     S: ReadStorage + Send + 'static,
 {
     async fn validate_transaction(
         &self,
-        storage: S,
+        storage: StorageWithOverrides<S>,
         env: OneshotEnv,
         tx: L2Tx,
         validation_params: ValidationParams,
@@ -152,23 +177,28 @@ where
             "Unexpected execution mode for tx validation: {:?} (expected `VerifyExecute`)",
             env.system.execution_mode
         );
-        let execution_latency_histogram = self.execution_latency_histogram;
+
+        let sandbox = VmSandbox {
+            fast_vm_mode: FastVmMode::Old,
+            panic_on_divergence: self.panic_on_divergence,
+            storage,
+            env,
+            execution_args: TxExecutionArgs::for_validation(tx),
+            execution_latency_histogram: self.execution_latency_histogram,
+        };
 
         tokio::task::spawn_blocking(move || {
             let (validation_tracer, mut validation_result) =
                 ValidationTracer::<HistoryDisabled>::new(
                     validation_params,
-                    env.system.version.into(),
+                    sandbox.env.system.version.into(),
                 );
             let tracers = vec![validation_tracer.into_tracer_pointer()];
 
-            let executor = VmSandbox::new(
-                storage,
-                env,
-                TxExecutionArgs::for_validation(tx),
-                execution_latency_histogram,
-            );
-            let exec_result = executor.apply(|vm, transaction| {
+            let exec_result = sandbox.execute_in_vm(|vm, transaction| {
+                let Vm::Legacy(vm) = vm else {
+                    unreachable!("Fast VM is never used for validation yet");
+                };
                 vm.push_transaction(transaction);
                 vm.inspect(&mut tracers.into(), InspectExecutionMode::OneTx)
             });
@@ -188,70 +218,99 @@ where
 }
 
 #[derive(Debug)]
-struct VmSandbox<S: ReadStorage> {
-    vm: Box<LegacyVmInstance<S, HistoryDisabled>>,
-    storage_view: StoragePtr<StorageView<S>>,
-    transaction: Transaction,
-    execution_latency_histogram: Option<&'static vise::Histogram<Duration>>,
+enum Vm<S: ReadStorage> {
+    Legacy(LegacyVmInstance<S, HistoryDisabled>),
+    Fast(FastVmInstance<S, ()>),
 }
 
-impl<S: ReadStorage> VmSandbox<S> {
-    /// This method is blocking.
-    fn new(
-        storage: S,
-        mut env: OneshotEnv,
-        execution_args: TxExecutionArgs,
-        execution_latency_histogram: Option<&'static vise::Histogram<Duration>>,
-    ) -> Self {
-        let mut storage_view = StorageView::new(storage);
-        Self::setup_storage_view(&mut storage_view, &execution_args, env.current_block);
-
-        let protocol_version = env.system.version;
-        if execution_args.adjust_pubdata_price {
-            env.l1_batch.fee_input = adjust_pubdata_price_for_tx(
-                env.l1_batch.fee_input,
-                execution_args.transaction.gas_per_pubdata_byte_limit(),
-                env.l1_batch.enforced_base_fee.map(U256::from),
-                protocol_version.into(),
-            );
+impl<S: ReadStorage> Vm<S> {
+    fn inspect_transaction_with_bytecode_compression(
+        &mut self,
+        missed_storage_invocation_limit: usize,
+        params: OneshotTracingParams,
+        tx: Transaction,
+        with_compression: bool,
+    ) -> OneshotTransactionExecutionResult {
+        let mut calls_result = Arc::<OnceCell<_>>::default();
+        let (compression_result, tx_result) = match self {
+            Self::Legacy(vm) => {
+                let mut tracers = Self::create_legacy_tracers(
+                    missed_storage_invocation_limit,
+                    params.trace_calls.then(|| calls_result.clone()),
+                );
+                vm.inspect_transaction_with_bytecode_compression(&mut tracers, tx, with_compression)
+            }
+            Self::Fast(vm) => {
+                assert!(
+                    !params.trace_calls,
+                    "Call tracing is not supported by fast VM yet"
+                );
+                let legacy_tracers = Self::create_legacy_tracers::<HistoryEnabled>(
+                    missed_storage_invocation_limit,
+                    None,
+                );
+                let mut full_tracer = (legacy_tracers.into(), ());
+                vm.inspect_transaction_with_bytecode_compression(
+                    &mut full_tracer,
+                    tx,
+                    with_compression,
+                )
+            }
         };
 
-        let storage_view = storage_view.to_rc_ptr();
-        let vm = Box::new(LegacyVmInstance::new_with_specific_version(
-            env.l1_batch,
-            env.system,
-            storage_view.clone(),
-            protocol_version.into_api_vm_version(),
-        ));
+        OneshotTransactionExecutionResult {
+            tx_result: Box::new(tx_result),
+            compression_result: compression_result.map(drop),
+            call_traces: Arc::make_mut(&mut calls_result).take().unwrap_or_default(),
+        }
+    }
 
-        Self {
-            vm,
-            storage_view,
-            transaction: execution_args.transaction,
-            execution_latency_histogram,
+    fn create_legacy_tracers<H: HistoryMode>(
+        missed_storage_invocation_limit: usize,
+        calls_result: Option<Arc<OnceCell<Vec<Call>>>>,
+    ) -> TracerDispatcher<StorageView<S>, H> {
+        let mut tracers = vec![];
+        if let Some(calls_result) = calls_result {
+            tracers.push(CallTracer::new(calls_result).into_tracer_pointer());
         }
+        tracers
+            .push(StorageInvocations::new(missed_storage_invocation_limit).into_tracer_pointer());
+        tracers.into()
     }
+}
 
+/// Full parameters necessary to instantiate a VM for oneshot execution.
+#[derive(Debug)]
+struct VmSandbox<S> {
+    fast_vm_mode: FastVmMode,
+    panic_on_divergence: bool,
+    storage: StorageWithOverrides<S>,
+    env: OneshotEnv,
+    execution_args: TxExecutionArgs,
+    execution_latency_histogram: Option<&'static vise::Histogram<Duration>>,
+}
+
+impl<S: ReadStorage> VmSandbox<S> {
     /// This method is blocking.
-    fn setup_storage_view(
-        storage_view: &mut StorageView<S>,
+    fn setup_storage(
+        storage: &mut StorageWithOverrides<S>,
         execution_args: &TxExecutionArgs,
         current_block: Option<StoredL2BlockEnv>,
     ) {
         let storage_view_setup_started_at = Instant::now();
         if let Some(nonce) = execution_args.enforced_nonce {
             let nonce_key = get_nonce_key(&execution_args.transaction.initiator_account());
-            let full_nonce = storage_view.read_value(&nonce_key);
+            let full_nonce = storage.read_value(&nonce_key);
             let (_, deployment_nonce) = decompose_full_nonce(h256_to_u256(full_nonce));
             let enforced_full_nonce = nonces_to_full_nonce(U256::from(nonce.0), deployment_nonce);
-            storage_view.set_value(nonce_key, u256_to_h256(enforced_full_nonce));
+            storage.set_value(nonce_key, u256_to_h256(enforced_full_nonce));
         }
 
         let payer = execution_args.transaction.payer();
         let balance_key = storage_key_for_eth_balance(&payer);
-        let mut current_balance = h256_to_u256(storage_view.read_value(&balance_key));
+        let mut current_balance = h256_to_u256(storage.read_value(&balance_key));
         current_balance += execution_args.added_balance;
-        storage_view.set_value(balance_key, u256_to_h256(current_balance));
+        storage.set_value(balance_key, u256_to_h256(current_balance));
 
         // Reset L2 block info if necessary.
         if let Some(current_block) = current_block {
@@ -261,13 +320,13 @@ impl<S: ReadStorage> VmSandbox<S> {
             );
             let l2_block_info =
                 pack_block_info(current_block.number.into(), current_block.timestamp);
-            storage_view.set_value(l2_block_info_key, u256_to_h256(l2_block_info));
+            storage.set_value(l2_block_info_key, u256_to_h256(l2_block_info));
 
             let l2_block_txs_rolling_hash_key = StorageKey::new(
                 AccountTreeId::new(SYSTEM_CONTEXT_ADDRESS),
                 SYSTEM_CONTEXT_CURRENT_TX_ROLLING_HASH_POSITION,
             );
-            storage_view.set_value(
+            storage.set_value(
                 l2_block_txs_rolling_hash_key,
                 current_block.txs_rolling_hash,
             );
@@ -280,30 +339,90 @@ impl<S: ReadStorage> VmSandbox<S> {
         }
     }
 
-    pub(super) fn apply<T, F>(mut self, apply_fn: F) -> T
-    where
-        F: FnOnce(&mut LegacyVmInstance<S, HistoryDisabled>, Transaction) -> T,
-    {
+    /// This method is blocking.
+    fn execute_in_vm<T>(
+        mut self,
+        action: impl FnOnce(&mut Vm<StorageWithOverrides<S>>, Transaction) -> T,
+    ) -> T {
+        Self::setup_storage(
+            &mut self.storage,
+            &self.execution_args,
+            self.env.current_block,
+        );
+
+        let protocol_version = self.env.system.version;
+        let mode = self.env.system.execution_mode;
+        if self.execution_args.adjust_pubdata_price {
+            self.env.l1_batch.fee_input = adjust_pubdata_price_for_tx(
+                self.env.l1_batch.fee_input,
+                self.execution_args.transaction.gas_per_pubdata_byte_limit(),
+                self.env.l1_batch.enforced_base_fee.map(U256::from),
+                protocol_version.into(),
+            );
+        };
+
+        let transaction = self.execution_args.transaction;
         let tx_id = format!(
             "{:?}-{}",
-            self.transaction.initiator_account(),
-            self.transaction.nonce().unwrap_or(Nonce(0))
+            transaction.initiator_account(),
+            transaction.nonce().unwrap_or(Nonce(0))
         );
 
+        let storage_view = StorageView::new(self.storage).to_rc_ptr();
+        let mut vm = match self.fast_vm_mode {
+            FastVmMode::Old => Vm::Legacy(LegacyVmInstance::new_with_specific_version(
+                self.env.l1_batch,
+                self.env.system,
+                storage_view.clone(),
+                protocol_version.into_api_vm_version(),
+            )),
+            FastVmMode::New => Vm::Fast(FastVmInstance::fast(
+                self.env.l1_batch,
+                self.env.system,
+                storage_view.clone(),
+            )),
+            FastVmMode::Shadow => {
+                let mut vm =
+                    ShadowVm::new(self.env.l1_batch, self.env.system, storage_view.clone());
+                if !self.panic_on_divergence {
+                    let transaction = format!("{:?}", transaction);
+                    let handler = DivergenceHandler::new(move |errors, _| {
+                        tracing::error!(transaction, ?mode, "{errors}");
+                    });
+                    vm.set_divergence_handler(handler);
+                }
+                Vm::Fast(FastVmInstance::Shadowed(vm))
+            }
+        };
+
         let started_at = Instant::now();
-        let result = apply_fn(&mut *self.vm, self.transaction);
+        let result = action(&mut vm, transaction);
         let vm_execution_took = started_at.elapsed();
 
         if let Some(histogram) = self.execution_latency_histogram {
             histogram.observe(vm_execution_took);
         }
-        let memory_metrics = self.vm.record_vm_memory_metrics();
-        metrics::report_vm_memory_metrics(
-            &tx_id,
-            &memory_metrics,
-            vm_execution_took,
-            &self.storage_view.borrow().stats(),
-        );
+
+        match &vm {
+            Vm::Legacy(vm) => {
+                let memory_metrics = vm.record_vm_memory_metrics();
+                metrics::report_vm_memory_metrics(
+                    &tx_id,
+                    &memory_metrics,
+                    vm_execution_took,
+                    &storage_view.borrow().stats(),
+                );
+            }
+            Vm::Fast(_) => {
+                // The new VM implementation doesn't have the same memory model as old ones, so it doesn't report memory metrics,
+                // only storage-related ones.
+                metrics::report_vm_storage_metrics(
+                    &format!("Tx {tx_id}"),
+                    vm_execution_took,
+                    &storage_view.borrow().stats(),
+                );
+            }
+        }
         result
     }
 }
diff --git a/core/lib/vm_executor/src/oneshot/tests.rs b/core/lib/vm_executor/src/oneshot/tests.rs
new file mode 100644
index 000000000000..65d2ff3727c0
--- /dev/null
+++ b/core/lib/vm_executor/src/oneshot/tests.rs
@@ -0,0 +1,107 @@
+//! Oneshot executor tests.
+
+use assert_matches::assert_matches;
+use test_casing::{test_casing, Product};
+use zksync_multivm::interface::storage::InMemoryStorage;
+use zksync_types::{ProtocolVersionId, H256};
+use zksync_utils::bytecode::hash_bytecode;
+
+use super::*;
+use crate::testonly::{
+    create_l2_transaction, default_l1_batch_env, default_system_env, FAST_VM_MODES,
+};
+
+const EXEC_MODES: [TxExecutionMode; 3] = [
+    TxExecutionMode::EstimateFee,
+    TxExecutionMode::EthCall,
+    TxExecutionMode::VerifyExecute,
+];
+
+#[test]
+fn selecting_vm_for_execution() {
+    let mut executor = MainOneshotExecutor::new(usize::MAX);
+    executor.set_fast_vm_mode(FastVmMode::New);
+
+    for exec_mode in EXEC_MODES {
+        let env = OneshotEnv {
+            system: default_system_env(exec_mode),
+            l1_batch: default_l1_batch_env(1),
+            current_block: None,
+        };
+        let mode = executor.select_fast_vm_mode(&env, &OneshotTracingParams::default());
+        assert_matches!(mode, FastVmMode::New);
+
+        // Tracing calls is not supported by the new VM.
+        let mode = executor.select_fast_vm_mode(&env, &OneshotTracingParams { trace_calls: true });
+        assert_matches!(mode, FastVmMode::Old);
+
+        // Old protocol versions are not supported either.
+        let mut old_env = env.clone();
+        old_env.system.version = ProtocolVersionId::Version22;
+        let mode = executor.select_fast_vm_mode(&old_env, &OneshotTracingParams::default());
+        assert_matches!(mode, FastVmMode::Old);
+    }
+}
+
+#[test]
+fn setting_up_nonce_and_balance_in_storage() {
+    let mut storage = StorageWithOverrides::new(InMemoryStorage::default());
+    let tx = create_l2_transaction(1_000_000_000.into(), Nonce(1));
+    let execution_args = TxExecutionArgs::for_gas_estimate(tx.clone().into());
+    VmSandbox::setup_storage(&mut storage, &execution_args, None);
+
+    // Check the overridden nonce and balance.
+    let nonce_key = get_nonce_key(&tx.initiator_account());
+    assert_eq!(storage.read_value(&nonce_key), H256::from_low_u64_be(1));
+    let balance_key = storage_key_for_eth_balance(&tx.initiator_account());
+    let expected_added_balance = tx.common_data.fee.gas_limit * tx.common_data.fee.max_fee_per_gas;
+    assert_eq!(
+        storage.read_value(&balance_key),
+        u256_to_h256(expected_added_balance)
+    );
+
+    let mut storage = InMemoryStorage::default();
+    storage.set_value(balance_key, H256::from_low_u64_be(2_000_000_000));
+    let mut storage = StorageWithOverrides::new(storage);
+    VmSandbox::setup_storage(&mut storage, &execution_args, None);
+
+    assert_eq!(
+        storage.read_value(&balance_key),
+        u256_to_h256(expected_added_balance + U256::from(2_000_000_000))
+    );
+}
+
+#[test_casing(9, Product((EXEC_MODES, FAST_VM_MODES)))]
+#[tokio::test]
+async fn inspecting_transfer(exec_mode: TxExecutionMode, fast_vm_mode: FastVmMode) {
+    let tx = create_l2_transaction(1_000_000_000.into(), Nonce(0));
+    let mut storage = InMemoryStorage::with_system_contracts(hash_bytecode);
+    storage.set_value(
+        storage_key_for_eth_balance(&tx.initiator_account()),
+        u256_to_h256(u64::MAX.into()),
+    );
+    let storage = StorageWithOverrides::new(storage);
+
+    let l1_batch = default_l1_batch_env(1);
+    let env = OneshotEnv {
+        system: default_system_env(exec_mode),
+        current_block: Some(StoredL2BlockEnv {
+            number: l1_batch.first_l2_block.number - 1,
+            timestamp: l1_batch.first_l2_block.timestamp - 1,
+            txs_rolling_hash: H256::zero(),
+        }),
+        l1_batch,
+    };
+    let args = TxExecutionArgs::for_gas_estimate(tx.into());
+    let tracing = OneshotTracingParams::default();
+
+    let mut executor = MainOneshotExecutor::new(usize::MAX);
+    executor.set_fast_vm_mode(fast_vm_mode);
+    let result = executor
+        .inspect_transaction_with_bytecode_compression(storage, env, args, tracing)
+        .await
+        .unwrap();
+    result.compression_result.unwrap();
+    let exec_result = result.tx_result.result;
+    assert!(!exec_result.is_failed(), "{exec_result:?}");
+}
diff --git a/core/lib/vm_executor/src/testonly.rs b/core/lib/vm_executor/src/testonly.rs
index 5bcd604a4324..2fa7f075db71 100644
--- a/core/lib/vm_executor/src/testonly.rs
+++ b/core/lib/vm_executor/src/testonly.rs
@@ -2,11 +2,14 @@ use once_cell::sync::Lazy;
 use zksync_contracts::BaseSystemContracts;
 use zksync_multivm::{
     interface::{L1BatchEnv, L2BlockEnv, SystemEnv, TxExecutionMode},
+    utils::derive_base_fee_and_gas_per_pubdata,
     vm_latest::constants::BATCH_COMPUTATIONAL_GAS_LIMIT,
+    zk_evm_latest::ethereum_types::U256,
 };
 use zksync_types::{
-    block::L2BlockHasher, fee_model::BatchFeeInput, vm::FastVmMode, Address, L1BatchNumber,
-    L2BlockNumber, L2ChainId, ProtocolVersionId, H256, ZKPORTER_IS_AVAILABLE,
+    block::L2BlockHasher, fee::Fee, fee_model::BatchFeeInput, l2::L2Tx,
+    transaction_request::PaymasterParams, vm::FastVmMode, Address, K256PrivateKey, L1BatchNumber,
+    L2BlockNumber, L2ChainId, Nonce, ProtocolVersionId, H256, ZKPORTER_IS_AVAILABLE,
 };
 
 static BASE_SYSTEM_CONTRACTS: Lazy<BaseSystemContracts> =
@@ -43,3 +46,28 @@ pub(crate) fn default_l1_batch_env(number: u32) -> L1BatchEnv {
         fee_input: BatchFeeInput::sensible_l1_pegged_default(),
     }
 }
+
+pub(crate) fn create_l2_transaction(value: U256, nonce: Nonce) -> L2Tx {
+    let (max_fee_per_gas, gas_per_pubdata_limit) = derive_base_fee_and_gas_per_pubdata(
+        BatchFeeInput::sensible_l1_pegged_default(),
+        ProtocolVersionId::latest().into(),
+    );
+    let fee = Fee {
+        gas_limit: 10_000_000.into(),
+        max_fee_per_gas: max_fee_per_gas.into(),
+        max_priority_fee_per_gas: 0_u64.into(),
+        gas_per_pubdata_limit: gas_per_pubdata_limit.into(),
+    };
+    L2Tx::new_signed(
+        Some(Address::random()),
+        vec![],
+        nonce,
+        fee,
+        value,
+        L2ChainId::default(),
+        &K256PrivateKey::random(),
+        vec![],
+        PaymasterParams::default(),
+    )
+    .unwrap()
+}
diff --git a/core/lib/vm_interface/src/storage/mod.rs b/core/lib/vm_interface/src/storage/mod.rs
index 6cdcd33db682..aade56ca5d96 100644
--- a/core/lib/vm_interface/src/storage/mod.rs
+++ b/core/lib/vm_interface/src/storage/mod.rs
@@ -5,11 +5,13 @@ use zksync_types::{get_known_code_key, StorageKey, StorageValue, H256};
 pub use self::{
     // Note, that `test_infra` of the bootloader tests relies on this value to be exposed
     in_memory::{InMemoryStorage, IN_MEMORY_STORAGE_DEFAULT_NETWORK_ID},
+    overrides::StorageWithOverrides,
     snapshot::{StorageSnapshot, StorageWithSnapshot},
     view::{ImmutableStorageView, StorageView, StorageViewCache, StorageViewStats},
 };
 
 mod in_memory;
+mod overrides;
 mod snapshot;
 mod view;
 
diff --git a/core/lib/vm_interface/src/storage/overrides.rs b/core/lib/vm_interface/src/storage/overrides.rs
new file mode 100644
index 000000000000..ad5a3d8624f1
--- /dev/null
+++ b/core/lib/vm_interface/src/storage/overrides.rs
@@ -0,0 +1,70 @@
+//! VM storage functionality specifically used in the VM sandbox.
+
+use std::{
+    collections::{HashMap, HashSet},
+    fmt,
+};
+
+use zksync_types::{AccountTreeId, StorageKey, StorageValue, H256};
+
+use super::ReadStorage;
+
+/// A storage view that allows to override some of the storage values.
+#[derive(Debug)]
+pub struct StorageWithOverrides<S> {
+    storage_handle: S,
+    overridden_slots: HashMap<StorageKey, H256>,
+    overridden_factory_deps: HashMap<H256, Vec<u8>>,
+    empty_accounts: HashSet<AccountTreeId>,
+}
+
+impl<S: ReadStorage> StorageWithOverrides<S> {
+    /// Creates a new storage view based on the underlying storage.
+    pub fn new(storage: S) -> Self {
+        Self {
+            storage_handle: storage,
+            overridden_slots: HashMap::new(),
+            overridden_factory_deps: HashMap::new(),
+            empty_accounts: HashSet::new(),
+        }
+    }
+
+    pub fn set_value(&mut self, key: StorageKey, value: StorageValue) {
+        self.overridden_slots.insert(key, value);
+    }
+
+    pub fn store_factory_dep(&mut self, hash: H256, code: Vec<u8>) {
+        self.overridden_factory_deps.insert(hash, code);
+    }
+
+    pub fn insert_erased_account(&mut self, account: AccountTreeId) {
+        self.empty_accounts.insert(account);
+    }
+}
+
+impl<S: ReadStorage + fmt::Debug> ReadStorage for StorageWithOverrides<S> {
+    fn read_value(&mut self, key: &StorageKey) -> StorageValue {
+        if let Some(value) = self.overridden_slots.get(key) {
+            return *value;
+        }
+        if self.empty_accounts.contains(key.account()) {
+            return H256::zero();
+        }
+        self.storage_handle.read_value(key)
+    }
+
+    fn is_write_initial(&mut self, key: &StorageKey) -> bool {
+        self.storage_handle.is_write_initial(key)
+    }
+
+    fn load_factory_dep(&mut self, hash: H256) -> Option<Vec<u8>> {
+        self.overridden_factory_deps
+            .get(&hash)
+            .cloned()
+            .or_else(|| self.storage_handle.load_factory_dep(hash))
+    }
+
+    fn get_enumeration_index(&mut self, key: &StorageKey) -> Option<u64> {
+        self.storage_handle.get_enumeration_index(key)
+    }
+}
diff --git a/core/lib/vm_interface/src/types/inputs/mod.rs b/core/lib/vm_interface/src/types/inputs/mod.rs
index cb80ba7c1386..83f87f0fe1dd 100644
--- a/core/lib/vm_interface/src/types/inputs/mod.rs
+++ b/core/lib/vm_interface/src/types/inputs/mod.rs
@@ -15,7 +15,7 @@ mod l2_block;
 mod system_env;
 
 /// Full environment for oneshot transaction / call execution.
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct OneshotEnv {
     /// System environment.
     pub system: SystemEnv,
diff --git a/core/node/api_server/src/execution_sandbox/execute.rs b/core/node/api_server/src/execution_sandbox/execute.rs
index bdd574625888..7958b5ed3c12 100644
--- a/core/node/api_server/src/execution_sandbox/execute.rs
+++ b/core/node/api_server/src/execution_sandbox/execute.rs
@@ -8,7 +8,7 @@ use tokio::runtime::Handle;
 use zksync_dal::{Connection, Core};
 use zksync_multivm::interface::{
     executor::{OneshotExecutor, TransactionValidator},
-    storage::ReadStorage,
+    storage::{ReadStorage, StorageWithOverrides},
     tracer::{ValidationError, ValidationParams},
     Call, OneshotEnv, OneshotTracingParams, OneshotTransactionExecutionResult,
     TransactionExecutionMetrics, TxExecutionArgs, VmExecutionResultAndLogs,
@@ -20,11 +20,10 @@ use zksync_types::{
 use zksync_vm_executor::oneshot::{MainOneshotExecutor, MockOneshotExecutor};
 
 use super::{
-    storage::StorageWithOverrides,
     vm_metrics::{self, SandboxStage},
     BlockArgs, VmPermit, SANDBOX_METRICS,
 };
-use crate::tx_sender::SandboxExecutorOptions;
+use crate::{execution_sandbox::storage::apply_state_override, tx_sender::SandboxExecutorOptions};
 
 /// Action that can be executed by [`SandboxExecutor`].
 #[derive(Debug)]
@@ -109,6 +108,9 @@ impl SandboxExecutor {
         missed_storage_invocation_limit: usize,
     ) -> Self {
         let mut executor = MainOneshotExecutor::new(missed_storage_invocation_limit);
+        executor.set_fast_vm_mode(options.fast_vm_mode);
+        #[cfg(test)]
+        executor.panic_on_divergence();
         executor
             .set_execution_latency_histogram(&SANDBOX_METRICS.sandbox[&SandboxStage::Execution]);
         Self {
@@ -151,7 +153,7 @@ impl SandboxExecutor {
             .await?;
 
         let state_override = state_override.unwrap_or_default();
-        let storage = StorageWithOverrides::new(storage, &state_override);
+        let storage = apply_state_override(storage, &state_override);
         let (execution_args, tracing_params) = action.into_parts();
         let result = self
             .inspect_transaction_with_bytecode_compression(
@@ -246,13 +248,13 @@ impl SandboxExecutor {
 }
 
 #[async_trait]
-impl<S> OneshotExecutor<S> for SandboxExecutor
+impl<S> OneshotExecutor<StorageWithOverrides<S>> for SandboxExecutor
 where
     S: ReadStorage + Send + 'static,
 {
     async fn inspect_transaction_with_bytecode_compression(
         &self,
-        storage: S,
+        storage: StorageWithOverrides<S>,
         env: OneshotEnv,
         args: TxExecutionArgs,
         tracing_params: OneshotTracingParams,
@@ -283,13 +285,13 @@ where
 }
 
 #[async_trait]
-impl<S> TransactionValidator<S> for SandboxExecutor
+impl<S> TransactionValidator<StorageWithOverrides<S>> for SandboxExecutor
 where
     S: ReadStorage + Send + 'static,
 {
     async fn validate_transaction(
         &self,
-        storage: S,
+        storage: StorageWithOverrides<S>,
         env: OneshotEnv,
         tx: L2Tx,
         validation_params: ValidationParams,
diff --git a/core/node/api_server/src/execution_sandbox/storage.rs b/core/node/api_server/src/execution_sandbox/storage.rs
index bf775d484906..c80356f6e36e 100644
--- a/core/node/api_server/src/execution_sandbox/storage.rs
+++ b/core/node/api_server/src/execution_sandbox/storage.rs
@@ -1,127 +1,67 @@
 //! VM storage functionality specifically used in the VM sandbox.
 
-use std::{
-    collections::{HashMap, HashSet},
-    fmt,
-};
-
-use zksync_multivm::interface::storage::ReadStorage;
+use zksync_multivm::interface::storage::{ReadStorage, StorageWithOverrides};
 use zksync_types::{
     api::state_override::{OverrideState, StateOverride},
     get_code_key, get_known_code_key, get_nonce_key,
     utils::{decompose_full_nonce, nonces_to_full_nonce, storage_key_for_eth_balance},
-    AccountTreeId, StorageKey, StorageValue, H256,
+    AccountTreeId, StorageKey, H256,
 };
 use zksync_utils::{h256_to_u256, u256_to_h256};
 
-/// A storage view that allows to override some of the storage values.
-#[derive(Debug)]
-pub(super) struct StorageWithOverrides<S> {
-    storage_handle: S,
-    overridden_slots: HashMap<StorageKey, H256>,
-    overridden_factory_deps: HashMap<H256, Vec<u8>>,
-    overridden_accounts: HashSet<AccountTreeId>,
-}
-
-impl<S: ReadStorage> StorageWithOverrides<S> {
-    /// Creates a new storage view based on the underlying storage.
-    pub(super) fn new(storage: S, state_override: &StateOverride) -> Self {
-        let mut this = Self {
-            storage_handle: storage,
-            overridden_slots: HashMap::new(),
-            overridden_factory_deps: HashMap::new(),
-            overridden_accounts: HashSet::new(),
-        };
-        this.apply_state_override(state_override);
-        this
-    }
-
-    fn apply_state_override(&mut self, state_override: &StateOverride) {
-        for (account, overrides) in state_override.iter() {
-            if let Some(balance) = overrides.balance {
-                let balance_key = storage_key_for_eth_balance(account);
-                self.overridden_slots
-                    .insert(balance_key, u256_to_h256(balance));
-            }
+/// This method is blocking.
+pub(super) fn apply_state_override<S: ReadStorage>(
+    storage: S,
+    state_override: &StateOverride,
+) -> StorageWithOverrides<S> {
+    let mut storage = StorageWithOverrides::new(storage);
+    for (account, overrides) in state_override.iter() {
+        if let Some(balance) = overrides.balance {
+            let balance_key = storage_key_for_eth_balance(account);
+            storage.set_value(balance_key, u256_to_h256(balance));
+        }
 
-            if let Some(nonce) = overrides.nonce {
-                let nonce_key = get_nonce_key(account);
-                let full_nonce = self.read_value(&nonce_key);
-                let (_, deployment_nonce) = decompose_full_nonce(h256_to_u256(full_nonce));
-                let new_full_nonce = u256_to_h256(nonces_to_full_nonce(nonce, deployment_nonce));
-                self.overridden_slots.insert(nonce_key, new_full_nonce);
-            }
+        if let Some(nonce) = overrides.nonce {
+            let nonce_key = get_nonce_key(account);
+            let full_nonce = storage.read_value(&nonce_key);
+            let (_, deployment_nonce) = decompose_full_nonce(h256_to_u256(full_nonce));
+            let new_full_nonce = u256_to_h256(nonces_to_full_nonce(nonce, deployment_nonce));
+            storage.set_value(nonce_key, new_full_nonce);
+        }
 
-            if let Some(code) = &overrides.code {
-                let code_key = get_code_key(account);
-                let code_hash = code.hash();
-                self.overridden_slots.insert(code_key, code_hash);
-                let known_code_key = get_known_code_key(&code_hash);
-                self.overridden_slots
-                    .insert(known_code_key, H256::from_low_u64_be(1));
-                self.store_factory_dep(code_hash, code.clone().into_bytes());
-            }
+        if let Some(code) = &overrides.code {
+            let code_key = get_code_key(account);
+            let code_hash = code.hash();
+            storage.set_value(code_key, code_hash);
+            let known_code_key = get_known_code_key(&code_hash);
+            storage.set_value(known_code_key, H256::from_low_u64_be(1));
+            storage.store_factory_dep(code_hash, code.clone().into_bytes());
+        }
 
-            match &overrides.state {
-                Some(OverrideState::State(state)) => {
-                    let account = AccountTreeId::new(*account);
-                    self.override_account_state_diff(account, state);
-                    self.overridden_accounts.insert(account);
+        match &overrides.state {
+            Some(OverrideState::State(state)) => {
+                let account = AccountTreeId::new(*account);
+                for (&key, &value) in state {
+                    storage.set_value(StorageKey::new(account, key), value);
                 }
-                Some(OverrideState::StateDiff(state_diff)) => {
-                    let account = AccountTreeId::new(*account);
-                    self.override_account_state_diff(account, state_diff);
+                storage.insert_erased_account(account);
+            }
+            Some(OverrideState::StateDiff(state_diff)) => {
+                let account = AccountTreeId::new(*account);
+                for (&key, &value) in state_diff {
+                    storage.set_value(StorageKey::new(account, key), value);
                 }
-                None => { /* do nothing */ }
             }
+            None => { /* do nothing */ }
         }
     }
-
-    fn store_factory_dep(&mut self, hash: H256, code: Vec<u8>) {
-        self.overridden_factory_deps.insert(hash, code);
-    }
-
-    fn override_account_state_diff(
-        &mut self,
-        account: AccountTreeId,
-        state_diff: &HashMap<H256, H256>,
-    ) {
-        let account_slots = state_diff
-            .iter()
-            .map(|(&slot, &value)| (StorageKey::new(account, slot), value));
-        self.overridden_slots.extend(account_slots);
-    }
-}
-
-impl<S: ReadStorage + fmt::Debug> ReadStorage for StorageWithOverrides<S> {
-    fn read_value(&mut self, key: &StorageKey) -> StorageValue {
-        if let Some(value) = self.overridden_slots.get(key) {
-            return *value;
-        }
-        if self.overridden_accounts.contains(key.account()) {
-            return H256::zero();
-        }
-        self.storage_handle.read_value(key)
-    }
-
-    fn is_write_initial(&mut self, key: &StorageKey) -> bool {
-        self.storage_handle.is_write_initial(key)
-    }
-
-    fn load_factory_dep(&mut self, hash: H256) -> Option<Vec<u8>> {
-        self.overridden_factory_deps
-            .get(&hash)
-            .cloned()
-            .or_else(|| self.storage_handle.load_factory_dep(hash))
-    }
-
-    fn get_enumeration_index(&mut self, key: &StorageKey) -> Option<u64> {
-        self.storage_handle.get_enumeration_index(key)
-    }
+    storage
 }
 
 #[cfg(test)]
 mod tests {
+    use std::collections::HashMap;
+
     use zksync_multivm::interface::storage::InMemoryStorage;
     use zksync_types::{
         api::state_override::{Bytecode, OverrideAccount},
@@ -184,7 +124,7 @@ mod tests {
         storage.set_value(retained_key, H256::repeat_byte(0xfe));
         let erased_key = StorageKey::new(AccountTreeId::new(Address::repeat_byte(5)), H256::zero());
         storage.set_value(erased_key, H256::repeat_byte(1));
-        let mut storage = StorageWithOverrides::new(storage, &overrides);
+        let mut storage = apply_state_override(storage, &overrides);
 
         let balance = storage.read_value(&storage_key_for_eth_balance(&Address::repeat_byte(1)));
         assert_eq!(balance, H256::from_low_u64_be(1));
diff --git a/core/node/api_server/src/execution_sandbox/validate.rs b/core/node/api_server/src/execution_sandbox/validate.rs
index 9a3c88f8bf0c..758547abbd6e 100644
--- a/core/node/api_server/src/execution_sandbox/validate.rs
+++ b/core/node/api_server/src/execution_sandbox/validate.rs
@@ -5,16 +5,15 @@ use tracing::Instrument;
 use zksync_dal::{Connection, Core, CoreDal};
 use zksync_multivm::interface::{
     executor::TransactionValidator,
+    storage::StorageWithOverrides,
     tracer::{ValidationError as RawValidationError, ValidationParams},
 };
 use zksync_types::{
-    api::state_override::StateOverride, fee_model::BatchFeeInput, l2::L2Tx, Address,
-    TRUSTED_ADDRESS_SLOTS, TRUSTED_TOKEN_SLOTS,
+    fee_model::BatchFeeInput, l2::L2Tx, Address, TRUSTED_ADDRESS_SLOTS, TRUSTED_TOKEN_SLOTS,
 };
 
 use super::{
     execute::{SandboxAction, SandboxExecutor},
-    storage::StorageWithOverrides,
     vm_metrics::{SandboxStage, EXECUTION_METRICS, SANDBOX_METRICS},
     BlockArgs, VmPermit,
 };
@@ -57,7 +56,7 @@ impl SandboxExecutor {
         let SandboxAction::Execution { tx, .. } = action else {
             unreachable!(); // by construction
         };
-        let storage = StorageWithOverrides::new(storage, &StateOverride::default());
+        let storage = StorageWithOverrides::new(storage);
 
         let stage_latency = SANDBOX_METRICS.sandbox[&SandboxStage::Validation].start();
         let validation_result = self
diff --git a/core/node/api_server/src/testonly.rs b/core/node/api_server/src/testonly.rs
index 6da8e333495f..3add9c2f165c 100644
--- a/core/node/api_server/src/testonly.rs
+++ b/core/node/api_server/src/testonly.rs
@@ -10,7 +10,7 @@ use zksync_contracts::{
 };
 use zksync_dal::{Connection, Core, CoreDal};
 use zksync_multivm::utils::derive_base_fee_and_gas_per_pubdata;
-use zksync_system_constants::L2_BASE_TOKEN_ADDRESS;
+use zksync_system_constants::{L2_BASE_TOKEN_ADDRESS, REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_BYTE};
 use zksync_types::{
     api::state_override::{Bytecode, OverrideAccount, OverrideState, StateOverride},
     ethabi,
@@ -18,11 +18,12 @@ use zksync_types::{
     fee::Fee,
     fee_model::FeeParams,
     get_code_key, get_known_code_key,
+    l1::L1Tx,
     l2::L2Tx,
-    transaction_request::{CallRequest, PaymasterParams},
+    transaction_request::{CallRequest, Eip712Meta, PaymasterParams},
     utils::storage_key_for_eth_balance,
     AccountTreeId, Address, K256PrivateKey, L2BlockNumber, L2ChainId, Nonce, ProtocolVersionId,
-    StorageKey, StorageLog, H256, U256,
+    StorageKey, StorageLog, EIP_712_TX_TYPE, H256, U256,
 };
 use zksync_utils::{address_to_u256, u256_to_h256};
 
@@ -343,6 +344,8 @@ pub(crate) trait TestAccount {
 
     fn create_counter_tx(&self, increment: U256, revert: bool) -> L2Tx;
 
+    fn create_l1_counter_tx(&self, increment: U256, revert: bool) -> L1Tx;
+
     fn query_counter_value(&self) -> CallRequest;
 
     fn create_infinite_loop_tx(&self) -> L2Tx;
@@ -482,6 +485,26 @@ impl TestAccount for K256PrivateKey {
         .unwrap()
     }
 
+    fn create_l1_counter_tx(&self, increment: U256, revert: bool) -> L1Tx {
+        let calldata = load_contract(COUNTER_CONTRACT_PATH)
+            .function("incrementWithRevert")
+            .expect("no `incrementWithRevert` function")
+            .encode_input(&[Token::Uint(increment), Token::Bool(revert)])
+            .expect("failed encoding `incrementWithRevert` input");
+        let request = CallRequest {
+            data: Some(calldata.into()),
+            from: Some(self.address()),
+            to: Some(StateBuilder::COUNTER_CONTRACT_ADDRESS),
+            transaction_type: Some(EIP_712_TX_TYPE.into()),
+            eip712_meta: Some(Eip712Meta {
+                gas_per_pubdata: REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_BYTE.into(),
+                ..Eip712Meta::default()
+            }),
+            ..CallRequest::default()
+        };
+        L1Tx::from_request(request, false).unwrap()
+    }
+
     fn query_counter_value(&self) -> CallRequest {
         let calldata = load_contract(COUNTER_CONTRACT_PATH)
             .function("get")
diff --git a/core/node/api_server/src/tx_sender/mod.rs b/core/node/api_server/src/tx_sender/mod.rs
index 38794fe71371..75cc1ad602f8 100644
--- a/core/node/api_server/src/tx_sender/mod.rs
+++ b/core/node/api_server/src/tx_sender/mod.rs
@@ -25,6 +25,7 @@ use zksync_types::{
     l2::{error::TxCheckError::TxDuplication, L2Tx},
     transaction_request::CallOverrides,
     utils::storage_key_for_eth_balance,
+    vm::FastVmMode,
     AccountTreeId, Address, L2ChainId, Nonce, ProtocolVersionId, Transaction, H160, H256,
     MAX_NEW_FACTORY_DEPS, U256,
 };
@@ -89,6 +90,7 @@ pub async fn build_tx_sender(
 /// Oneshot executor options used by the API server sandbox.
 #[derive(Debug)]
 pub struct SandboxExecutorOptions {
+    pub(crate) fast_vm_mode: FastVmMode,
     /// Env parameters to be used when estimating gas.
     pub(crate) estimate_gas: OneshotEnvParameters<EstimateGas>,
     /// Env parameters to be used when performing `eth_call` requests.
@@ -114,6 +116,7 @@ impl SandboxExecutorOptions {
                 .context("failed loading base contracts for calls / tx execution")?;
 
         Ok(Self {
+            fast_vm_mode: FastVmMode::Old,
             estimate_gas: OneshotEnvParameters::new(
                 Arc::new(estimate_gas_contracts),
                 chain_id,
@@ -129,6 +132,11 @@ impl SandboxExecutorOptions {
         })
     }
 
+    /// Sets the fast VM mode used by this executor.
+    pub fn set_fast_vm_mode(&mut self, fast_vm_mode: FastVmMode) {
+        self.fast_vm_mode = fast_vm_mode;
+    }
+
     pub(crate) async fn mock() -> Self {
         Self::new(L2ChainId::default(), AccountTreeId::default(), u32::MAX)
             .await
diff --git a/core/node/api_server/src/tx_sender/tests/gas_estimation.rs b/core/node/api_server/src/tx_sender/tests/gas_estimation.rs
index 4528d9cda12f..7db1b8339314 100644
--- a/core/node/api_server/src/tx_sender/tests/gas_estimation.rs
+++ b/core/node/api_server/src/tx_sender/tests/gas_estimation.rs
@@ -74,6 +74,28 @@ async fn initial_estimate_for_load_test_transaction(tx_params: LoadnextContractE
     test_initial_estimate(state_override, tx, DEFAULT_MULTIPLIER).await;
 }
 
+#[tokio::test]
+async fn initial_gas_estimate_for_l1_transaction() {
+    let alice = K256PrivateKey::random();
+    let state_override = StateBuilder::default().with_counter_contract(0).build();
+    let tx = alice.create_l1_counter_tx(1.into(), false);
+
+    let pool = ConnectionPool::<Core>::constrained_test_pool(1).await;
+    let tx_sender = create_real_tx_sender(pool).await;
+    let block_args = pending_block_args(&tx_sender).await;
+    let mut estimator = GasEstimator::new(&tx_sender, tx.into(), block_args, Some(state_override))
+        .await
+        .unwrap();
+    estimator.adjust_transaction_fee();
+    let initial_estimate = estimator.initialize().await.unwrap();
+    assert!(initial_estimate.total_gas_charged.is_none());
+
+    let (vm_result, _) = estimator.unadjusted_step(15_000).await.unwrap();
+    assert!(vm_result.result.is_failed(), "{:?}", vm_result.result);
+    let (vm_result, _) = estimator.unadjusted_step(1_000_000).await.unwrap();
+    assert!(!vm_result.result.is_failed(), "{:?}", vm_result.result);
+}
+
 #[test_casing(2, [false, true])]
 #[tokio::test]
 async fn initial_estimate_for_deep_recursion(with_reads: bool) {
@@ -322,9 +344,10 @@ async fn insufficient_funds_error_for_transfer() {
 
 async fn test_estimating_gas(
     state_override: StateOverride,
-    tx: L2Tx,
+    tx: impl Into<Transaction>,
     acceptable_overestimation: u64,
 ) {
+    let tx = tx.into();
     let pool = ConnectionPool::<Core>::constrained_test_pool(1).await;
     let tx_sender = create_real_tx_sender(pool).await;
     let block_args = pending_block_args(&tx_sender).await;
@@ -332,7 +355,7 @@ async fn test_estimating_gas(
     let fee_scale_factor = 1.0;
     let fee = tx_sender
         .get_txs_fee_in_wei(
-            tx.clone().into(),
+            tx.clone(),
             block_args.clone(),
             fee_scale_factor,
             acceptable_overestimation,
@@ -350,7 +373,7 @@ async fn test_estimating_gas(
 
     let fee = tx_sender
         .get_txs_fee_in_wei(
-            tx.into(),
+            tx,
             block_args,
             fee_scale_factor,
             acceptable_overestimation,
@@ -383,6 +406,15 @@ async fn estimating_gas_for_transfer(acceptable_overestimation: u64) {
     test_estimating_gas(state_override, tx, acceptable_overestimation).await;
 }
 
+#[tokio::test]
+async fn estimating_gas_for_l1_transaction() {
+    let alice = K256PrivateKey::random();
+    let state_override = StateBuilder::default().with_counter_contract(0).build();
+    let tx = alice.create_l1_counter_tx(1.into(), false);
+
+    test_estimating_gas(state_override, tx, 0).await;
+}
+
 #[test_casing(10, Product((LOAD_TEST_CASES, [0, 100])))]
 #[tokio::test]
 async fn estimating_gas_for_load_test_tx(
diff --git a/core/node/api_server/src/tx_sender/tests/mod.rs b/core/node/api_server/src/tx_sender/tests/mod.rs
index cacd616202d2..ea3f77fbcd82 100644
--- a/core/node/api_server/src/tx_sender/tests/mod.rs
+++ b/core/node/api_server/src/tx_sender/tests/mod.rs
@@ -145,13 +145,14 @@ async fn create_real_tx_sender(pool: ConnectionPool<Core>) -> TxSender {
     drop(storage);
 
     let genesis_config = genesis_params.config();
-    let executor_options = SandboxExecutorOptions::new(
+    let mut executor_options = SandboxExecutorOptions::new(
         genesis_config.l2_chain_id,
         AccountTreeId::new(genesis_config.fee_account),
         u32::MAX,
     )
     .await
     .unwrap();
+    executor_options.set_fast_vm_mode(FastVmMode::Shadow);
 
     let pg_caches = PostgresStorageCaches::new(1, 1);
     let tx_executor = SandboxExecutor::real(executor_options, pg_caches, usize::MAX);
diff --git a/core/node/api_server/src/web3/tests/vm.rs b/core/node/api_server/src/web3/tests/vm.rs
index 45128f579cda..7dd0164198a1 100644
--- a/core/node/api_server/src/web3/tests/vm.rs
+++ b/core/node/api_server/src/web3/tests/vm.rs
@@ -16,8 +16,8 @@ use zksync_multivm::interface::{
 };
 use zksync_types::{
     api::ApiStorageLog, fee_model::BatchFeeInput, get_intrinsic_constants,
-    transaction_request::CallRequest, K256PrivateKey, L2ChainId, PackedEthSignature,
-    StorageLogKind, StorageLogWithPreviousValue, Transaction, U256,
+    transaction_request::CallRequest, vm::FastVmMode, K256PrivateKey, L2ChainId,
+    PackedEthSignature, StorageLogKind, StorageLogWithPreviousValue, Transaction, U256,
 };
 use zksync_utils::u256_to_h256;
 use zksync_vm_executor::oneshot::{
@@ -92,6 +92,7 @@ impl<C: ContractsKind> BaseSystemContractsProvider<C> for BaseContractsWithMockE
 fn executor_options_with_evm_emulator() -> SandboxExecutorOptions {
     let base_contracts = Arc::<BaseContractsWithMockEvmEmulator>::default();
     SandboxExecutorOptions {
+        fast_vm_mode: FastVmMode::Old,
         estimate_gas: OneshotEnvParameters::new(
             base_contracts.clone(),
             L2ChainId::default(),
diff --git a/core/node/consensus/src/vm.rs b/core/node/consensus/src/vm.rs
index 46b84c34061d..cbd4918dcee1 100644
--- a/core/node/consensus/src/vm.rs
+++ b/core/node/consensus/src/vm.rs
@@ -11,7 +11,8 @@ use zksync_vm_executor::oneshot::{
     CallOrExecute, MainOneshotExecutor, MultiVMBaseSystemContracts, OneshotEnvParameters,
 };
 use zksync_vm_interface::{
-    executor::OneshotExecutor, ExecutionResult, OneshotTracingParams, TxExecutionArgs,
+    executor::OneshotExecutor, storage::StorageWithOverrides, ExecutionResult,
+    OneshotTracingParams, TxExecutionArgs,
 };
 
 use crate::{abi, storage::ConnectionPool};
@@ -89,7 +90,7 @@ impl VM {
 
         let output = ctx
             .wait(self.executor.inspect_transaction_with_bytecode_compression(
-                storage,
+                StorageWithOverrides::new(storage),
                 env,
                 TxExecutionArgs::for_eth_call(tx),
                 OneshotTracingParams::default(),
diff --git a/core/node/node_framework/src/implementations/layers/web3_api/tx_sender.rs b/core/node/node_framework/src/implementations/layers/web3_api/tx_sender.rs
index ba1a69e23bb6..023ef1059c79 100644
--- a/core/node/node_framework/src/implementations/layers/web3_api/tx_sender.rs
+++ b/core/node/node_framework/src/implementations/layers/web3_api/tx_sender.rs
@@ -6,7 +6,7 @@ use zksync_node_api_server::{
     tx_sender::{SandboxExecutorOptions, TxSenderBuilder, TxSenderConfig},
 };
 use zksync_state::{PostgresStorageCaches, PostgresStorageCachesTask};
-use zksync_types::{AccountTreeId, Address};
+use zksync_types::{vm::FastVmMode, AccountTreeId, Address};
 use zksync_web3_decl::{
     client::{DynClient, L2},
     jsonrpsee,
@@ -60,6 +60,7 @@ pub struct TxSenderLayer {
     postgres_storage_caches_config: PostgresStorageCachesConfig,
     max_vm_concurrency: usize,
     whitelisted_tokens_for_aa_cache: bool,
+    vm_mode: FastVmMode,
 }
 
 #[derive(Debug, FromContext)]
@@ -95,6 +96,7 @@ impl TxSenderLayer {
             postgres_storage_caches_config,
             max_vm_concurrency,
             whitelisted_tokens_for_aa_cache: false,
+            vm_mode: FastVmMode::Old,
         }
     }
 
@@ -106,6 +108,12 @@ impl TxSenderLayer {
         self.whitelisted_tokens_for_aa_cache = value;
         self
     }
+
+    /// Sets the fast VM modes used for all supported operations.
+    pub fn with_vm_mode(mut self, mode: FastVmMode) -> Self {
+        self.vm_mode = mode;
+        self
+    }
 }
 
 #[async_trait::async_trait]
@@ -151,12 +159,13 @@ impl WiringLayer for TxSenderLayer {
 
         // TODO (BFT-138): Allow to dynamically reload API contracts
         let config = self.tx_sender_config;
-        let executor_options = SandboxExecutorOptions::new(
+        let mut executor_options = SandboxExecutorOptions::new(
             config.chain_id,
             AccountTreeId::new(config.fee_account_addr),
             config.validation_computational_gas_limit,
         )
         .await?;
+        executor_options.set_fast_vm_mode(self.vm_mode);
 
         // Build `TxSender`.
         let mut tx_sender = TxSenderBuilder::new(config, replica_pool, tx_sink);
diff --git a/etc/env/file_based/overrides/tests/integration.yaml b/etc/env/file_based/overrides/tests/integration.yaml
new file mode 100644
index 000000000000..6ad031e29458
--- /dev/null
+++ b/etc/env/file_based/overrides/tests/integration.yaml
@@ -0,0 +1,4 @@
+experimental_vm:
+  # Use the shadow VM mode everywhere to catch divergences as early as possible
+  state_keeper_fast_vm_mode: SHADOW
+  api_fast_vm_mode: SHADOW
diff --git a/etc/env/file_based/overrides/tests/loadtest-new.yaml b/etc/env/file_based/overrides/tests/loadtest-new.yaml
index 2167f7347e09..e66625636b1f 100644
--- a/etc/env/file_based/overrides/tests/loadtest-new.yaml
+++ b/etc/env/file_based/overrides/tests/loadtest-new.yaml
@@ -1,7 +1,11 @@
 db:
   merkle_tree:
     mode: LIGHTWEIGHT
+api:
+  web3_json_rpc:
+    estimate_gas_optimize_search: true
 experimental_vm:
   state_keeper_fast_vm_mode: NEW
+  api_fast_vm_mode: NEW
 mempool:
   delay_interval: 50
diff --git a/etc/env/file_based/overrides/tests/loadtest-old.yaml b/etc/env/file_based/overrides/tests/loadtest-old.yaml
index a2d66d1cf4a7..7b1a35870187 100644
--- a/etc/env/file_based/overrides/tests/loadtest-old.yaml
+++ b/etc/env/file_based/overrides/tests/loadtest-old.yaml
@@ -3,5 +3,6 @@ db:
     mode: LIGHTWEIGHT
 experimental_vm:
   state_keeper_fast_vm_mode: OLD
+  api_fast_vm_mode: OLD
 mempool:
   delay_interval: 50