Skip to main content

safe_rs/simulation/
fork.rs

1//! Fork database and revm simulation
2
3use std::collections::BTreeMap;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use std::time::{SystemTime, UNIX_EPOCH};
7
8use alloy::network::AnyNetwork;
9use alloy::primitives::{Address, Bytes, Log, TxKind, B256, U256};
10use alloy::providers::Provider;
11use alloy::rpc::types::trace::geth::pre_state::{AccountState, DiffMode};
12use serde::Serialize;
13use foundry_fork_db::{cache::BlockchainDbMeta, BlockchainDb, SharedBackend};
14use revm::context::TxEnv;
15use revm::database::CacheDB;
16use revm::primitives::hardfork::SpecId;
17use revm::state::EvmState;
18use revm::Database;
19use revm::{Context, ExecuteEvm, MainBuilder, MainContext};
20use revm_inspectors::tracing::{TracingInspector, TracingInspectorConfig};
21
22pub use revm_inspectors::tracing::CallTraceArena;
23
24use crate::error::{Error, Result};
25use crate::types::Operation;
26
27/// Result of a simulated transaction
28#[derive(Debug, Clone)]
29pub struct SimulationResult {
30    /// Whether the simulation succeeded
31    pub success: bool,
32    /// Gas used during simulation
33    pub gas_used: u64,
34    /// Return data from the call
35    pub return_data: Bytes,
36    /// Logs emitted during simulation
37    pub logs: Vec<Log>,
38    /// Revert reason if the call reverted
39    pub revert_reason: Option<String>,
40    /// State changes from simulation (pre/post state for touched accounts)
41    pub state_diff: DiffMode,
42    /// Call trace arena (if tracing was enabled)
43    pub traces: Option<CallTraceArena>,
44}
45
46impl SimulationResult {
47    /// Returns true if the simulation was successful
48    pub fn is_success(&self) -> bool {
49        self.success
50    }
51
52    /// Returns the revert reason if available
53    pub fn error_message(&self) -> Option<&str> {
54        self.revert_reason.as_deref()
55    }
56
57    /// Format traces as human-readable text (cast run style)
58    ///
59    /// Returns `None` if tracing was not enabled for this simulation.
60    pub fn format_traces(&self) -> Option<String> {
61        use revm_inspectors::tracing::TraceWriter;
62
63        let traces = self.traces.as_ref()?;
64        let mut writer = TraceWriter::new(Vec::<u8>::new());
65        writer.write_arena(traces).ok()?;
66        String::from_utf8(writer.into_writer()).ok()
67    }
68}
69
70/// Debug output for a failed simulation, written to disk as JSON
71#[derive(Debug, Serialize)]
72pub struct SimulationDebugOutput {
73    /// ISO 8601 timestamp
74    pub timestamp: String,
75    /// Chain ID
76    pub chain_id: u64,
77    /// The account address (Safe or EOA)
78    pub account_address: Address,
79    /// The call that was attempted
80    pub call: CallDebugInfo,
81    /// The simulation result
82    pub result: SimulationResultDebug,
83}
84
85/// Debug information about a call
86#[derive(Debug, Serialize)]
87pub struct CallDebugInfo {
88    /// Target address
89    pub to: Address,
90    /// ETH value sent
91    pub value: String,
92    /// Calldata (hex encoded)
93    pub data: String,
94    /// Operation type (Call or DelegateCall)
95    pub operation: String,
96}
97
98/// Debug information about a simulation result (serializable version)
99#[derive(Debug, Serialize)]
100pub struct SimulationResultDebug {
101    /// Whether the simulation succeeded
102    pub success: bool,
103    /// Gas used
104    pub gas_used: u64,
105    /// Revert reason if the call reverted
106    pub revert_reason: Option<String>,
107    /// Return data (hex encoded)
108    pub return_data: String,
109    /// Logs emitted during simulation
110    pub logs: Vec<LogDebug>,
111    /// State diff
112    pub state_diff: StateDiffDebug,
113    /// Formatted traces if available
114    pub traces: Option<String>,
115}
116
117/// Debug information about a log entry
118#[derive(Debug, Serialize)]
119pub struct LogDebug {
120    /// Address that emitted the log
121    pub address: Address,
122    /// Topics
123    pub topics: Vec<String>,
124    /// Data (hex encoded)
125    pub data: String,
126}
127
128/// Debug information about state diff
129#[derive(Debug, Serialize)]
130pub struct StateDiffDebug {
131    /// Pre-state of affected accounts
132    pub pre: BTreeMap<Address, AccountStateDebug>,
133    /// Post-state of affected accounts
134    pub post: BTreeMap<Address, AccountStateDebug>,
135}
136
137/// Debug information about account state
138#[derive(Debug, Serialize)]
139pub struct AccountStateDebug {
140    /// Account balance
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub balance: Option<String>,
143    /// Account nonce
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub nonce: Option<u64>,
146    /// Storage slots (key -> value, both hex encoded)
147    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
148    pub storage: BTreeMap<String, String>,
149}
150
151impl SimulationDebugOutput {
152    /// Creates a new debug output from a simulation result and context
153    pub fn new(
154        chain_id: u64,
155        account_address: Address,
156        to: Address,
157        value: U256,
158        data: &Bytes,
159        operation: &crate::types::Operation,
160        result: &SimulationResult,
161    ) -> Self {
162        let timestamp = chrono::Utc::now().to_rfc3339();
163
164        let call = CallDebugInfo {
165            to,
166            value: value.to_string(),
167            data: format!("0x{}", alloy::primitives::hex::encode(data)),
168            operation: format!("{:?}", operation),
169        };
170
171        let logs = result
172            .logs
173            .iter()
174            .map(|log| LogDebug {
175                address: log.address,
176                topics: log
177                    .topics()
178                    .iter()
179                    .map(|t| format!("0x{}", alloy::primitives::hex::encode(t)))
180                    .collect(),
181                data: format!("0x{}", alloy::primitives::hex::encode(log.data.data.as_ref())),
182            })
183            .collect();
184
185        let state_diff = StateDiffDebug {
186            pre: result
187                .state_diff
188                .pre
189                .iter()
190                .map(|(addr, state)| (*addr, AccountStateDebug::from(state)))
191                .collect(),
192            post: result
193                .state_diff
194                .post
195                .iter()
196                .map(|(addr, state)| (*addr, AccountStateDebug::from(state)))
197                .collect(),
198        };
199
200        let result_debug = SimulationResultDebug {
201            success: result.success,
202            gas_used: result.gas_used,
203            revert_reason: result.revert_reason.clone(),
204            return_data: format!("0x{}", alloy::primitives::hex::encode(&result.return_data)),
205            logs,
206            state_diff,
207            traces: result.format_traces(),
208        };
209
210        Self {
211            timestamp,
212            chain_id,
213            account_address,
214            call,
215            result: result_debug,
216        }
217    }
218
219    /// Writes the debug output to a file in the given directory.
220    ///
221    /// The filename format is: `{chain_id}-{address}-{timestamp}.json`
222    ///
223    /// Creates the directory if it doesn't exist.
224    pub fn write_to_dir(&self, dir: &Path) -> std::io::Result<PathBuf> {
225        // Create directory if it doesn't exist
226        std::fs::create_dir_all(dir)?;
227
228        // Generate filename: {chain_id}-{address}-{timestamp}.json
229        let timestamp = SystemTime::now()
230            .duration_since(UNIX_EPOCH)
231            .unwrap_or_default()
232            .as_secs();
233        let filename = format!(
234            "{}-{}-{}.json",
235            self.chain_id,
236            self.account_address.to_string().to_lowercase(),
237            timestamp
238        );
239        let path = dir.join(filename);
240
241        // Write JSON to file
242        let json = serde_json::to_string_pretty(self)
243            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
244        std::fs::write(&path, json)?;
245
246        Ok(path)
247    }
248}
249
250impl From<&AccountState> for AccountStateDebug {
251    fn from(state: &AccountState) -> Self {
252        Self {
253            balance: state.balance.map(|b| b.to_string()),
254            nonce: state.nonce,
255            storage: state
256                .storage
257                .iter()
258                .map(|(k, v)| {
259                    (
260                        format!("0x{}", alloy::primitives::hex::encode(k)),
261                        format!("0x{}", alloy::primitives::hex::encode(v)),
262                    )
263                })
264                .collect(),
265        }
266    }
267}
268
269/// Builds a state diff from REVM's execution state
270///
271/// REVM tracks original values in `Account.original_info` and `EvmStorageSlot.original_value`,
272/// so we can reconstruct both pre and post state from the final state.
273fn build_state_diff(state: &EvmState) -> DiffMode {
274    let mut pre = BTreeMap::new();
275    let mut post = BTreeMap::new();
276
277    for (address, account) in state.iter() {
278        // Skip if account wasn't touched
279        if !account.is_touched() {
280            continue;
281        }
282
283        // Build storage diffs - only include changed slots
284        let mut pre_storage = BTreeMap::new();
285        let mut post_storage = BTreeMap::new();
286
287        for (key, slot) in account.storage.iter() {
288            if slot.is_changed() {
289                pre_storage.insert(B256::from(*key), B256::from(slot.original_value));
290                post_storage.insert(B256::from(*key), B256::from(slot.present_value));
291            }
292        }
293
294        // Build pre-state from original_info
295        let pre_state = AccountState {
296            balance: Some(account.original_info.balance),
297            nonce: Some(account.original_info.nonce),
298            code: account
299                .original_info
300                .code
301                .as_ref()
302                .map(|c| Bytes::from(c.original_bytes().to_vec())),
303            storage: pre_storage,
304        };
305
306        // Build post-state from current info
307        let post_state = AccountState {
308            balance: Some(account.info.balance),
309            nonce: Some(account.info.nonce),
310            code: account
311                .info
312                .code
313                .as_ref()
314                .map(|c| Bytes::from(c.original_bytes().to_vec())),
315            storage: post_storage,
316        };
317
318        pre.insert(*address, pre_state);
319        post.insert(*address, post_state);
320    }
321
322    DiffMode { pre, post }
323}
324
325/// Fork simulator for executing transactions against a forked state
326pub struct ForkSimulator<P> {
327    provider: P,
328    chain_id: u64,
329    block_number: Option<u64>,
330    tracing: bool,
331    caller_balance: Option<U256>,
332    debug_output_dir: Option<PathBuf>,
333    /// Account address for debug output (the Safe or EOA address)
334    debug_account_address: Option<Address>,
335}
336
337impl<P> ForkSimulator<P>
338where
339    P: Provider<AnyNetwork> + Clone + 'static,
340{
341    /// Creates a new fork simulator
342    pub fn new(provider: P, chain_id: u64) -> Self {
343        Self {
344            provider,
345            chain_id,
346            block_number: None,
347            tracing: false,
348            caller_balance: None,
349            debug_output_dir: None,
350            debug_account_address: None,
351        }
352    }
353
354    /// Configures a directory for writing debug output on simulation failures.
355    ///
356    /// When a simulation fails and this is set, a JSON file will be written
357    /// to the configured directory with the simulation details.
358    ///
359    /// The `account_address` is the Safe or EOA address that will be recorded
360    /// in the debug output.
361    pub fn with_debug_output_dir(mut self, dir: impl Into<PathBuf>, account_address: Address) -> Self {
362        self.debug_output_dir = Some(dir.into());
363        self.debug_account_address = Some(account_address);
364        self
365    }
366
367    /// Sets the block number to fork from
368    pub fn at_block(mut self, block: u64) -> Self {
369        self.block_number = Some(block);
370        self
371    }
372
373    /// Enables transaction tracing (cast run style)
374    ///
375    /// When enabled, `simulate_call()` will capture detailed call traces
376    /// showing the nested call hierarchy, gas per call, and call/return data.
377    /// Access traces via `SimulationResult::format_traces()`.
378    pub fn with_tracing(mut self, enable: bool) -> Self {
379        self.tracing = enable;
380        self
381    }
382
383    /// Sets a custom balance for the caller during simulation.
384    ///
385    /// If not set, the caller's on-chain balance is used.
386    pub fn with_caller_balance(mut self, balance: U256) -> Self {
387        self.caller_balance = Some(balance);
388        self
389    }
390
391    /// Creates a forked database from the current provider state
392    pub async fn create_fork_db(&self) -> Result<CacheDB<SharedBackend>> {
393        let block = match self.block_number {
394            Some(b) => b,
395            None => self
396                .provider
397                .get_block_number()
398                .await
399                .map_err(|e| Error::ForkDb(e.to_string()))?,
400        };
401
402        let meta = BlockchainDbMeta::new(
403            Default::default(), // empty known contracts
404            format!("fork-{}", self.chain_id),
405        );
406
407        let db = BlockchainDb::new(meta, None);
408        let backend = SharedBackend::spawn_backend_thread(
409            Arc::new(self.provider.clone()),
410            db,
411            Some(block.into()),
412        );
413
414        Ok(CacheDB::new(backend))
415    }
416
417    /// Simulates a call from the Safe
418    pub async fn simulate_call(
419        &self,
420        from: Address,
421        to: Address,
422        value: U256,
423        data: Bytes,
424        operation: Operation,
425    ) -> Result<SimulationResult> {
426        let mut db = self.create_fork_db().await?;
427
428        // Only override caller balance if configured
429        // Use load_account to preserve existing account info (code, nonce, code_hash)
430        if let Some(balance) = self.caller_balance {
431            let existing_account = db
432                .load_account(from)
433                .map_err(|e| Error::ForkDb(format!("Failed to load caller account: {:?}", e)))?;
434            existing_account.info.balance = balance;
435        }
436
437        // Fetch the caller's actual nonce from the forked database
438        let caller_nonce = db
439            .basic(from)
440            .map_err(|e| Error::ForkDb(format!("Failed to fetch caller info: {:?}", e)))?
441            .map(|info| info.nonce)
442            .unwrap_or(0);
443
444        // Determine the actual call target and calldata
445        let (call_to, call_data) = match operation {
446            Operation::Call => (to, data.to_vec()),
447            Operation::DelegateCall => {
448                // For delegatecall simulation, we execute directly from the Safe
449                // This is a simplification - in reality the Safe would delegatecall
450                (to, data.to_vec())
451            }
452        };
453
454        let tx = TxEnv {
455            caller: from,
456            gas_limit: 30_000_000,
457            gas_price: 0,
458            kind: TxKind::Call(call_to),
459            value,
460            data: call_data.into(),
461            nonce: caller_nonce,
462            chain_id: Some(self.chain_id),
463            ..Default::default()
464        };
465
466        // Build the EVM context
467        let ctx = Context::mainnet()
468            .with_db(db)
469            .modify_cfg_chained(|cfg| {
470                cfg.spec = SpecId::CANCUN;
471                cfg.chain_id = self.chain_id;
472                // Allow simulation from contract addresses (e.g., Safe contracts)
473                cfg.disable_eip3607 = true;
474            })
475            .modify_block_chained(|block| {
476                block.basefee = 0;
477            })
478            .with_tx(tx.clone());
479
480        let sim_result = if self.tracing {
481            // Create inspector for tracing
482            let config = TracingInspectorConfig::default_parity();
483            let mut inspector = TracingInspector::new(config);
484
485            // Build EVM with inspector attached and execute
486            let mut evm = ctx.build_mainnet_with_inspector(&mut inspector);
487            let result = evm.transact(tx).map_err(|e| Error::Revm(format!("{:?}", e)))?;
488
489            // Extract traces from the inspector
490            let traces = Some(inspector.into_traces());
491
492            let mut sim_result = self.process_result(result);
493            sim_result.traces = traces;
494            sim_result
495        } else {
496            // Create and run the EVM without tracing
497            let mut evm = ctx.build_mainnet();
498            let result = evm.transact(tx).map_err(|e| Error::Revm(format!("{:?}", e)))?;
499
500            self.process_result(result)
501        };
502
503        // Write debug output if simulation failed and debug output is configured
504        if !sim_result.success {
505            if let (Some(dir), Some(account_address)) =
506                (&self.debug_output_dir, self.debug_account_address)
507            {
508                let debug_output = SimulationDebugOutput::new(
509                    self.chain_id,
510                    account_address,
511                    to,
512                    value,
513                    &data,
514                    &operation,
515                    &sim_result,
516                );
517                // Best-effort write - don't fail the simulation if we can't write debug output
518                let _ = debug_output.write_to_dir(dir);
519            }
520        }
521
522        Ok(sim_result)
523    }
524
525    /// Estimates gas for a Safe internal call
526    ///
527    /// Runs the simulation and returns gas used + 10% buffer
528    pub async fn estimate_safe_tx_gas(
529        &self,
530        from: Address,
531        to: Address,
532        value: U256,
533        data: Bytes,
534        operation: Operation,
535    ) -> Result<U256> {
536        let result = self.simulate_call(from, to, value, data, operation).await?;
537
538        if !result.success {
539            return Err(Error::GasEstimation(format!(
540                "Simulation failed: {}",
541                result.revert_reason.unwrap_or_else(|| "unknown".to_string())
542            )));
543        }
544
545        // Add 10% buffer to the gas used
546        let gas_with_buffer = result.gas_used + (result.gas_used / 10);
547        Ok(U256::from(gas_with_buffer))
548    }
549
550    fn process_result<H>(
551        &self,
552        result: revm::context::result::ExecResultAndState<revm::context::result::ExecutionResult<H>>,
553    ) -> SimulationResult
554    where
555        H: std::fmt::Debug,
556    {
557        use revm::context::result::{ExecutionResult, Output};
558
559        // Build state diff from the execution state
560        let state_diff = build_state_diff(&result.state);
561
562        match result.result {
563            ExecutionResult::Success {
564                gas_used,
565                output,
566                logs,
567                ..
568            } => {
569                let return_data = match output {
570                    Output::Call(data) => Bytes::from(data.to_vec()),
571                    Output::Create(_, _) => Bytes::new(),
572                };
573
574                let logs = logs
575                    .into_iter()
576                    .filter_map(|log| {
577                        Log::new(log.address, log.topics().to_vec(), log.data.data.clone())
578                    })
579                    .collect();
580
581                SimulationResult {
582                    success: true,
583                    gas_used,
584                    return_data,
585                    logs,
586                    revert_reason: None,
587                    state_diff,
588                    traces: None,
589                }
590            }
591            ExecutionResult::Revert { gas_used, output } => {
592                let revert_reason = Self::decode_revert_reason(&output);
593                SimulationResult {
594                    success: false,
595                    gas_used,
596                    return_data: Bytes::from(output.to_vec()),
597                    logs: vec![],
598                    revert_reason: Some(revert_reason),
599                    state_diff,
600                    traces: None,
601                }
602            }
603            ExecutionResult::Halt { gas_used, reason } => SimulationResult {
604                success: false,
605                gas_used,
606                return_data: Bytes::new(),
607                logs: vec![],
608                revert_reason: Some(format!("Halted: {:?}", reason)),
609                state_diff,
610                traces: None,
611            },
612        }
613    }
614
615    fn decode_revert_reason(output: &revm::primitives::Bytes) -> String {
616        if output.len() < 4 {
617            return "Unknown revert".to_string();
618        }
619
620        // Check for Error(string) selector: 0x08c379a0
621        if output[0..4] == [0x08, 0xc3, 0x79, 0xa0] && output.len() >= 68 {
622            // Skip selector (4) + offset (32) + length position
623            let offset = 4 + 32;
624            if output.len() > offset + 32 {
625                let len = u32::from_be_bytes([
626                    output[offset + 28],
627                    output[offset + 29],
628                    output[offset + 30],
629                    output[offset + 31],
630                ]) as usize;
631
632                let str_start = offset + 32;
633                if output.len() >= str_start + len {
634                    if let Ok(s) = String::from_utf8(output[str_start..str_start + len].to_vec()) {
635                        return s;
636                    }
637                }
638            }
639        }
640
641        // Check for Panic(uint256) selector: 0x4e487b71
642        if output[0..4] == [0x4e, 0x48, 0x7b, 0x71] && output.len() >= 36 {
643            let panic_code =
644                u32::from_be_bytes([output[32], output[33], output[34], output[35]]) as usize;
645            return match panic_code {
646                0x00 => "Panic: generic/compiler panic",
647                0x01 => "Panic: assertion failed",
648                0x11 => "Panic: arithmetic overflow/underflow",
649                0x12 => "Panic: division by zero",
650                0x21 => "Panic: invalid enum value",
651                0x22 => "Panic: access to incorrectly encoded storage",
652                0x31 => "Panic: pop on empty array",
653                0x32 => "Panic: array out of bounds",
654                0x41 => "Panic: memory overflow",
655                0x51 => "Panic: call to zero-initialized function",
656                _ => "Panic: unknown code",
657            }
658            .to_string();
659        }
660
661        format!("Revert: 0x{}", alloy::primitives::hex::encode(output))
662    }
663}
664
665#[cfg(test)]
666mod tests {
667    use super::*;
668
669    #[test]
670    fn test_simulation_result() {
671        let result = SimulationResult {
672            success: true,
673            gas_used: 21000,
674            return_data: Bytes::new(),
675            logs: vec![],
676            revert_reason: None,
677            state_diff: DiffMode::default(),
678            traces: None,
679        };
680
681        assert!(result.is_success());
682        assert!(result.error_message().is_none());
683        assert!(result.format_traces().is_none());
684    }
685
686    #[test]
687    fn test_simulation_result_revert() {
688        let result = SimulationResult {
689            success: false,
690            gas_used: 21000,
691            return_data: Bytes::new(),
692            logs: vec![],
693            revert_reason: Some("ERC20: insufficient balance".to_string()),
694            state_diff: DiffMode::default(),
695            traces: None,
696        };
697
698        assert!(!result.is_success());
699        assert_eq!(result.error_message(), Some("ERC20: insufficient balance"));
700    }
701
702    #[test]
703    fn test_state_diff_with_balance_change() {
704        let mut pre = BTreeMap::new();
705        let mut post = BTreeMap::new();
706
707        let addr = Address::ZERO;
708
709        pre.insert(
710            addr,
711            AccountState {
712                balance: Some(U256::from(1000)),
713                nonce: Some(0),
714                code: None,
715                storage: BTreeMap::new(),
716            },
717        );
718
719        post.insert(
720            addr,
721            AccountState {
722                balance: Some(U256::from(500)),
723                nonce: Some(1),
724                code: None,
725                storage: BTreeMap::new(),
726            },
727        );
728
729        let state_diff = DiffMode { pre, post };
730
731        let result = SimulationResult {
732            success: true,
733            gas_used: 21000,
734            return_data: Bytes::new(),
735            logs: vec![],
736            revert_reason: None,
737            state_diff,
738            traces: None,
739        };
740
741        assert!(result.is_success());
742        assert_eq!(result.state_diff.pre.len(), 1);
743        assert_eq!(result.state_diff.post.len(), 1);
744
745        let pre_account = result.state_diff.pre.get(&addr).unwrap();
746        let post_account = result.state_diff.post.get(&addr).unwrap();
747
748        assert_eq!(pre_account.balance, Some(U256::from(1000)));
749        assert_eq!(post_account.balance, Some(U256::from(500)));
750        assert_eq!(pre_account.nonce, Some(0));
751        assert_eq!(post_account.nonce, Some(1));
752    }
753
754    #[test]
755    fn test_state_diff_with_storage_change() {
756        let mut pre = BTreeMap::new();
757        let mut post = BTreeMap::new();
758
759        let addr = Address::ZERO;
760        let storage_key = B256::ZERO;
761
762        // Storage values in AccountState are B256, not U256
763        let pre_value = B256::from(U256::from(100));
764        let post_value = B256::from(U256::from(200));
765
766        let mut pre_storage = BTreeMap::new();
767        pre_storage.insert(storage_key, pre_value);
768
769        let mut post_storage = BTreeMap::new();
770        post_storage.insert(storage_key, post_value);
771
772        pre.insert(
773            addr,
774            AccountState {
775                balance: Some(U256::ZERO),
776                nonce: Some(0),
777                code: None,
778                storage: pre_storage,
779            },
780        );
781
782        post.insert(
783            addr,
784            AccountState {
785                balance: Some(U256::ZERO),
786                nonce: Some(0),
787                code: None,
788                storage: post_storage,
789            },
790        );
791
792        let state_diff = DiffMode { pre, post };
793
794        let result = SimulationResult {
795            success: true,
796            gas_used: 50000,
797            return_data: Bytes::new(),
798            logs: vec![],
799            revert_reason: None,
800            state_diff,
801            traces: None,
802        };
803
804        let pre_account = result.state_diff.pre.get(&addr).unwrap();
805        let post_account = result.state_diff.post.get(&addr).unwrap();
806
807        assert_eq!(pre_account.storage.get(&storage_key), Some(&pre_value));
808        assert_eq!(post_account.storage.get(&storage_key), Some(&post_value));
809    }
810}