Skip to main content

cast/cmd/safe/
execute.rs

1use super::{
2    contracts::ISafe,
3    service::{SafeServiceOpts, SafeTransaction},
4    transaction::send_safe_call,
5};
6use alloy_network::Ethereum;
7use alloy_primitives::{Address, B256, Bytes, U256};
8use alloy_provider::Provider;
9use alloy_rpc_types::Log;
10use alloy_sol_types::{SolCall, SolEvent};
11use eyre::{Context, Result, ensure};
12use foundry_cli::{
13    json::print_scalar,
14    opts::{RpcOpts, TransactionOpts},
15    utils::LoadConfig,
16};
17use foundry_common::{provider::ProviderBuilder, sh_status};
18use foundry_wallets::WalletOpts;
19
20#[allow(clippy::too_many_arguments)]
21pub(super) async fn run(
22    safe: Address,
23    safe_tx_hash: B256,
24    confirmations: u64,
25    timeout: Option<u64>,
26    poll_interval: Option<u64>,
27    service: SafeServiceOpts,
28    rpc: RpcOpts,
29    wallet: WalletOpts,
30    tx: TransactionOpts,
31) -> Result<()> {
32    let config = rpc.load_config()?;
33    let read_provider = ProviderBuilder::<Ethereum>::from_config(&config)?.build()?;
34    let chain_id = read_provider.get_chain_id().await?;
35    let transaction = service.get_transaction(chain_id, "v2", safe_tx_hash).await?;
36    ensure!(
37        !transaction.is_executed && transaction.transaction_hash.is_none(),
38        "Safe transaction has already been executed{}",
39        transaction.transaction_hash.map(|hash| format!(" onchain as {hash}")).unwrap_or_default()
40    );
41
42    transaction.verify_hash(safe, &read_provider).await?;
43    ensure_current_nonce(
44        safe,
45        SafeTransaction::number(&transaction.nonce, "nonce")?,
46        &read_provider,
47    )
48    .await?;
49    transaction.show_transaction_summary()?;
50
51    let signatures = transaction.packed_signatures()?;
52
53    sh_status!("Executing Safe transaction {safe_tx_hash}")?;
54    let calldata: Bytes = ISafe::execTransactionCall {
55        to: transaction.to,
56        value: SafeTransaction::number(&transaction.value, "value")?,
57        data: transaction.data.clone(),
58        operation: transaction.operation,
59        safeTxGas: SafeTransaction::number(&transaction.safe_tx_gas, "safeTxGas")?,
60        baseGas: SafeTransaction::number(&transaction.base_gas, "baseGas")?,
61        gasPrice: SafeTransaction::number(&transaction.gas_price, "gasPrice")?,
62        gasToken: transaction.gas_token,
63        refundReceiver: transaction.refund_receiver,
64        signatures,
65    }
66    .abi_encode()
67    .into();
68    let result =
69        send_safe_call(safe, calldata, confirmations, timeout, poll_interval, rpc, wallet, tx)
70            .await
71            .wrap_err("failed to submit Safe transaction")?;
72    ensure!(
73        execution_succeeded(&result.logs, safe, safe_tx_hash)?,
74        "Safe inner transaction failed"
75    );
76    print_scalar(result.tx_hash)?;
77    Ok(())
78}
79
80async fn ensure_current_nonce<P>(safe: Address, transaction_nonce: U256, provider: &P) -> Result<()>
81where
82    P: Provider<Ethereum>,
83{
84    let current_nonce =
85        ISafe::new(safe, provider).nonce().call().await.wrap_err("failed to read Safe nonce")?;
86    ensure!(
87        transaction_nonce == current_nonce,
88        "Safe transaction nonce {transaction_nonce} does not match current Safe nonce {current_nonce}"
89    );
90    Ok(())
91}
92
93fn execution_succeeded(logs: &[Log], safe: Address, safe_tx_hash: B256) -> Result<bool> {
94    logs.iter()
95        .rev()
96        .filter(|log| log.address() == safe)
97        .find_map(|log| {
98            if let Ok(event) = ISafe::ExecutionSuccess::decode_log(&log.inner)
99                && event.txHash == safe_tx_hash
100            {
101                return Some(true);
102            }
103            if let Ok(event) = ISafe::ExecutionFailure::decode_log(&log.inner)
104                && event.txHash == safe_tx_hash
105            {
106                return Some(false);
107            }
108            None
109        })
110        .ok_or_else(|| eyre::eyre!("Safe execution receipt did not emit a matching result event"))
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use alloy_provider::{ProviderBuilder, mock::Asserter};
117
118    fn success(safe: Address, hash: B256) -> Log {
119        Log {
120            inner: alloy_primitives::Log::new_from_event_unchecked(
121                safe,
122                ISafe::ExecutionSuccess { txHash: hash, payment: U256::ZERO },
123            )
124            .reserialize(),
125            ..Default::default()
126        }
127    }
128
129    fn failure(safe: Address, hash: B256) -> Log {
130        Log {
131            inner: alloy_primitives::Log::new_from_event_unchecked(
132                safe,
133                ISafe::ExecutionFailure { txHash: hash, payment: U256::ZERO },
134            )
135            .reserialize(),
136            ..Default::default()
137        }
138    }
139
140    #[test]
141    fn uses_last_matching_safe_execution_event() {
142        let safe = Address::repeat_byte(1);
143        let hash = B256::repeat_byte(2);
144
145        assert!(
146            execution_succeeded(&[failure(safe, hash), success(safe, hash)], safe, hash).unwrap()
147        );
148        assert!(
149            !execution_succeeded(&[success(safe, hash), failure(safe, hash)], safe, hash).unwrap()
150        );
151    }
152
153    #[tokio::test]
154    async fn requires_transaction_nonce_to_be_current() {
155        let safe = Address::repeat_byte(1);
156        let asserter = Asserter::new();
157        asserter.push_success(&B256::from(U256::from(7).to_be_bytes::<32>()));
158        let provider = ProviderBuilder::new().connect_mocked_client(asserter);
159
160        ensure_current_nonce(safe, U256::from(7), &provider).await.unwrap();
161
162        let asserter = Asserter::new();
163        asserter.push_success(&B256::from(U256::from(8).to_be_bytes::<32>()));
164        let provider = ProviderBuilder::new().connect_mocked_client(asserter);
165        let error = ensure_current_nonce(safe, U256::from(7), &provider).await.unwrap_err();
166        assert_eq!(
167            error.to_string(),
168            "Safe transaction nonce 7 does not match current Safe nonce 8"
169        );
170    }
171}