Skip to main content

cast/cmd/safe/
simulate.rs

1use super::{
2    contracts::{ISafe, ISimulateTxAccessor, SIMULATE_TX_ACCESSOR_V1_4_1},
3    deploy::ensure_contract,
4    rpc_provider,
5    service::{SafeServiceOpts, SafeTransaction},
6};
7use alloy_network::TransactionBuilder;
8use alloy_primitives::{Address, B256, Bytes, U256, hex};
9use alloy_provider::Provider;
10use alloy_rpc_types::TransactionRequest;
11use alloy_sol_types::{SolCall, SolValue};
12use clap::Args;
13use eyre::{Context, Result, ensure};
14use foundry_cli::{json::print_json_object, opts::RpcOpts};
15use serde_json::json;
16
17/// CLI arguments for `cast safe simulate`.
18#[derive(Args, Debug)]
19pub struct SimulateArgs {
20    /// Safe account address.
21    safe: Address,
22
23    /// Safe transaction hash from the Transaction Service.
24    safe_tx_hash: B256,
25
26    /// Address that will execute the Safe transaction. Used as the simulation's tx.origin.
27    #[arg(long, env = "ETH_FROM", value_name = "ADDRESS")]
28    from: Address,
29
30    /// SimulateTxAccessor address.
31    #[arg(long, default_value_t = SIMULATE_TX_ACCESSOR_V1_4_1)]
32    accessor: Address,
33
34    #[command(flatten)]
35    service: Box<SafeServiceOpts>,
36
37    #[command(flatten)]
38    rpc: Box<RpcOpts>,
39}
40
41impl SimulateArgs {
42    pub(super) async fn run(self) -> Result<()> {
43        let Self { safe, safe_tx_hash, from, accessor, service, rpc } = self;
44        let (provider, chain_id) = rpc_provider(&rpc).await?;
45        let transaction = service.get_transaction(chain_id, "v2", safe_tx_hash).await?;
46        transaction.verify_hash(safe, &provider).await?;
47        transaction.show_transaction_summary()?;
48        ensure!(
49            SafeTransaction::number(&transaction.gas_price, "gasPrice")?.is_zero(),
50            "cannot simulate reimbursed Safe transactions (gasPrice > 0): SimulateTxAccessor does not enforce safeTxGas"
51        );
52        ensure_contract(&provider, accessor, "SimulateTxAccessor", "--accessor").await?;
53
54        let accessor_call = ISimulateTxAccessor::simulateCall {
55            to: transaction.to,
56            value: SafeTransaction::number(&transaction.value, "value")?,
57            data: transaction.data.clone(),
58            operation: transaction.operation,
59        }
60        .abi_encode();
61        let simulation_call = ISafe::simulateAndRevertCall {
62            targetContract: accessor,
63            calldataPayload: accessor_call.into(),
64        }
65        .abi_encode();
66        let request =
67            TransactionRequest::default().with_from(from).with_to(safe).with_input(simulation_call);
68        let Err(error) = provider.call(request).await else {
69            eyre::bail!("Safe simulateAndRevert unexpectedly returned successfully");
70        };
71        let revert_data = error
72            .as_error_resp()
73            .and_then(|payload| payload.as_revert_data())
74            .ok_or_else(|| eyre::eyre!("Safe simulation failed without revert data: {error}"))?;
75        let (gas_used, success, return_data) = decode_result(&revert_data)?;
76        ensure!(
77            success,
78            "Safe transaction simulation failed after {gas_used} gas; return data: {return_data}"
79        );
80        print_json_object(json!({
81            "safeTxHash": safe_tx_hash,
82            "success": true,
83            "gasUsed": gas_used.to_string(),
84            "returnData": return_data,
85        }))
86    }
87}
88
89fn decode_result(revert_data: &[u8]) -> Result<(U256, bool, Bytes)> {
90    let header_length = 2 * U256::BYTES;
91    ensure!(revert_data.len() >= header_length, "invalid Safe simulateAndRevert response");
92    let accessor_success = !U256::from_be_slice(&revert_data[..U256::BYTES]).is_zero();
93    let response_len = U256::from_be_slice(&revert_data[U256::BYTES..header_length]);
94    let available = revert_data.len() - header_length;
95    ensure!(
96        response_len <= U256::from(available),
97        "invalid Safe simulateAndRevert response length"
98    );
99    let response = &revert_data[header_length..header_length + response_len.to::<usize>()];
100    ensure!(
101        accessor_success,
102        "SimulateTxAccessor delegatecall failed; return data: {}",
103        hex::encode_prefixed(response)
104    );
105    <(U256, bool, Bytes)>::abi_decode_params(response)
106        .wrap_err("invalid SimulateTxAccessor response")
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn decodes_signature_independent_simulation_result() {
115        let return_data = Bytes::from_static(b"result");
116        let accessor_response = (U256::from(42), true, return_data.clone()).abi_encode_params();
117        let mut revert_data = Vec::with_capacity(2 * U256::BYTES + accessor_response.len());
118        revert_data.extend_from_slice(&U256::from(1).to_be_bytes::<{ U256::BYTES }>());
119        revert_data.extend_from_slice(
120            &U256::from(accessor_response.len()).to_be_bytes::<{ U256::BYTES }>(),
121        );
122        revert_data.extend_from_slice(&accessor_response);
123
124        let decoded = decode_result(&revert_data).unwrap();
125        assert_eq!(decoded, (U256::from(42), true, return_data));
126    }
127}