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}