Skip to main content

cast/cmd/safe/
execute.rs

1use super::{
2    contracts::ISafe,
3    rpc_provider,
4    service::{SafeServiceOpts, SafeTransaction},
5    transaction::SafeSendOpts,
6};
7use alloy_primitives::{Address, B256};
8use alloy_rpc_types::Log;
9use alloy_sol_types::{SolCall, SolEvent};
10use clap::Args;
11use eyre::{Context, Result, ensure};
12use foundry_cli::json::print_scalar;
13use foundry_common::sh_status;
14
15/// CLI arguments for `cast safe execute`.
16#[derive(Args, Debug)]
17pub struct ExecuteArgs {
18    /// Safe account address.
19    safe: Address,
20
21    /// Safe transaction hash from the Transaction Service.
22    safe_tx_hash: B256,
23
24    /// Number of confirmations to wait for.
25    #[arg(long, default_value = "1")]
26    confirmations: u64,
27
28    /// Timeout for execution confirmation, in seconds.
29    #[arg(long, env = "ETH_TIMEOUT")]
30    timeout: Option<u64>,
31
32    /// Polling interval for the execution receipt, in seconds.
33    #[arg(long, env = "ETH_POLL_INTERVAL")]
34    poll_interval: Option<u64>,
35
36    #[command(flatten)]
37    service: Box<SafeServiceOpts>,
38
39    #[command(flatten)]
40    send: SafeSendOpts,
41}
42
43impl ExecuteArgs {
44    pub(super) async fn run(self) -> Result<()> {
45        let Self { safe, safe_tx_hash, confirmations, timeout, poll_interval, service, send } =
46            self;
47        let (provider, chain_id) = rpc_provider(&send.rpc).await?;
48        let transaction = service.get_transaction(chain_id, "v2", safe_tx_hash).await?;
49        ensure!(
50            !transaction.is_executed && transaction.transaction_hash.is_none(),
51            "Safe transaction has already been executed{}",
52            transaction
53                .transaction_hash
54                .map(|hash| format!(" onchain as {hash}"))
55                .unwrap_or_default()
56        );
57
58        transaction.verify_hash(safe, &provider).await?;
59        let transaction_nonce = SafeTransaction::number(&transaction.nonce, "nonce")?;
60        let current_nonce = ISafe::new(safe, &provider)
61            .nonce()
62            .call()
63            .await
64            .wrap_err("failed to read Safe nonce")?;
65        ensure!(
66            transaction_nonce == current_nonce,
67            "Safe transaction nonce {transaction_nonce} does not match current Safe nonce {current_nonce}"
68        );
69        transaction.show_transaction_summary()?;
70        let signatures = transaction.packed_signatures()?;
71
72        sh_status!("Executing Safe transaction {safe_tx_hash}")?;
73        let calldata = ISafe::execTransactionCall {
74            to: transaction.to,
75            value: SafeTransaction::number(&transaction.value, "value")?,
76            data: transaction.data.clone(),
77            operation: transaction.operation,
78            safeTxGas: SafeTransaction::number(&transaction.safe_tx_gas, "safeTxGas")?,
79            baseGas: SafeTransaction::number(&transaction.base_gas, "baseGas")?,
80            gasPrice: SafeTransaction::number(&transaction.gas_price, "gasPrice")?,
81            gasToken: transaction.gas_token,
82            refundReceiver: transaction.refund_receiver,
83            signatures,
84        }
85        .abi_encode()
86        .into();
87        let result = send
88            .send(safe, calldata, confirmations, timeout, poll_interval)
89            .await
90            .wrap_err("failed to submit Safe transaction")?;
91        ensure!(
92            execution_succeeded(&result.logs, safe, safe_tx_hash)?,
93            "Safe inner transaction failed"
94        );
95        print_scalar(result.tx_hash)
96    }
97}
98
99/// Returns the outcome of the last `ExecutionSuccess`/`ExecutionFailure` event for `safe_tx_hash`.
100fn execution_succeeded(logs: &[Log], safe: Address, safe_tx_hash: B256) -> Result<bool> {
101    logs.iter()
102        .rev()
103        .filter(|log| log.address() == safe)
104        .find_map(|log| {
105            if let Ok(event) = ISafe::ExecutionSuccess::decode_log(&log.inner)
106                && event.txHash == safe_tx_hash
107            {
108                return Some(true);
109            }
110            if let Ok(event) = ISafe::ExecutionFailure::decode_log(&log.inner)
111                && event.txHash == safe_tx_hash
112            {
113                return Some(false);
114            }
115            None
116        })
117        .ok_or_else(|| eyre::eyre!("Safe execution receipt did not emit a matching result event"))
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use alloy_primitives::{LogData, U256};
124
125    fn log(safe: Address, data: LogData) -> Log {
126        Log { inner: alloy_primitives::Log { address: safe, data }, ..Default::default() }
127    }
128
129    #[test]
130    fn uses_last_matching_safe_execution_event() {
131        let safe = Address::repeat_byte(1);
132        let hash = B256::repeat_byte(2);
133        let success = || {
134            log(
135                safe,
136                ISafe::ExecutionSuccess { txHash: hash, payment: U256::ZERO }.encode_log_data(),
137            )
138        };
139        let failure = || {
140            log(
141                safe,
142                ISafe::ExecutionFailure { txHash: hash, payment: U256::ZERO }.encode_log_data(),
143            )
144        };
145
146        assert!(execution_succeeded(&[failure(), success()], safe, hash).unwrap());
147        assert!(!execution_succeeded(&[success(), failure()], safe, hash).unwrap());
148    }
149}