Skip to main content

safe_rs/
safe.rs

1//! Safe client and SafeBuilder implementation
2
3use std::path::{Path, PathBuf};
4
5use alloy::network::primitives::ReceiptResponse;
6use alloy::network::{AnyNetwork, Network};
7use alloy::primitives::{Address, Bytes, TxHash, U256};
8use alloy::providers::Provider;
9use alloy::signers::local::PrivateKeySigner;
10use alloy::sol_types::SolCall;
11
12use crate::account::Account;
13use crate::chain::{ChainAddresses, ChainConfig};
14use crate::contracts::{IMultiSend, IMultiSendCallOnly, ISafe};
15use crate::encoding::{compute_safe_transaction_hash, encode_multisend_data, SafeTxParams};
16use crate::error::{Error, Result};
17use crate::signing::sign_hash;
18use crate::simulation::{ForkSimulator, SimulationResult};
19use crate::types::{Call, CallBuilder, Operation};
20
21/// Safe proxy singleton storage slot (slot 0)
22/// Safe proxies store the implementation/singleton address at storage slot 0,
23/// as the first declared variable in the proxy contract.
24pub const SAFE_SINGLETON_SLOT: U256 = U256::ZERO;
25
26/// Checks if an address is a Safe contract by reading the singleton storage slot
27/// and matching against known Safe singleton addresses.
28///
29/// Safe proxies store the implementation address at storage slot 0 (not ERC1967).
30///
31/// # Arguments
32/// * `provider` - The provider for RPC calls
33/// * `address` - The address to check
34///
35/// # Returns
36/// `true` if the address is a Safe proxy pointing to a known Safe singleton,
37/// `false` otherwise (including if the address has no code or no implementation slot).
38pub async fn is_safe<P: Provider<N>, N: Network>(
39    provider: &P,
40    address: Address,
41) -> Result<bool> {
42    // Read the Safe singleton slot (slot 0)
43    let storage_value = provider
44        .get_storage_at(address, SAFE_SINGLETON_SLOT)
45        .await
46        .map_err(|e| Error::Fetch {
47            what: "singleton slot",
48            reason: e.to_string(),
49        })?;
50
51    // Parse storage value as an address (last 20 bytes of the 32-byte slot)
52    let impl_address = Address::from_slice(&storage_value.to_be_bytes::<32>()[12..]);
53
54    // Check against known Safe singletons
55    let v1_4_1 = ChainAddresses::v1_4_1();
56    let v1_3_0 = ChainAddresses::v1_3_0();
57
58    Ok(impl_address == v1_4_1.safe_singleton || impl_address == v1_3_0.safe_singleton)
59}
60
61/// Result of executing a Safe transaction
62#[derive(Debug, Clone)]
63pub struct ExecutionResult {
64    /// Transaction hash
65    pub tx_hash: TxHash,
66    /// Whether the Safe transaction succeeded (not just inclusion)
67    pub success: bool,
68}
69
70/// Safe client for interacting with Safe v1.4.1 smart accounts
71pub struct Safe<P> {
72    /// The provider for RPC calls
73    provider: P,
74    /// The signer for transactions
75    signer: PrivateKeySigner,
76    /// The Safe contract address
77    address: Address,
78    /// Chain configuration
79    config: ChainConfig,
80    /// Debug output directory for simulation failures
81    debug_output_dir: Option<PathBuf>,
82}
83
84impl<P> Safe<P>
85where
86    P: Provider<AnyNetwork> + Clone + 'static,
87{
88    /// Creates a new Safe client
89    pub fn new(provider: P, signer: PrivateKeySigner, address: Address, config: ChainConfig) -> Self {
90        Self {
91            provider,
92            signer,
93            address,
94            config,
95            debug_output_dir: None,
96        }
97    }
98
99    /// Configures a directory for writing debug output on simulation failures.
100    ///
101    /// When a simulation fails and this is set, a JSON file will be written
102    /// to the configured directory with the simulation details.
103    pub fn with_debug_output_dir(mut self, path: impl Into<PathBuf>) -> Self {
104        self.debug_output_dir = Some(path.into());
105        self
106    }
107
108    /// Creates a Safe client with auto-detected chain configuration
109    pub async fn connect(provider: P, signer: PrivateKeySigner, address: Address) -> Result<Self> {
110        let chain_id = provider
111            .get_chain_id()
112            .await
113            .map_err(|e| Error::Provider(e.to_string()))?;
114
115        let config = ChainConfig::new(chain_id);
116        Ok(Self::new(provider, signer, address, config))
117    }
118
119    /// Returns the chain addresses
120    pub fn addresses(&self) -> &ChainAddresses {
121        &self.config.addresses
122    }
123
124    /// Gets the threshold of the Safe
125    pub async fn threshold(&self) -> Result<u64> {
126        let safe = ISafe::new(self.address, &self.provider);
127        let threshold = safe
128            .getThreshold()
129            .call()
130            .await
131            .map_err(|e| Error::Fetch {
132                what: "threshold",
133                reason: e.to_string(),
134            })?;
135        Ok(threshold.to::<u64>())
136    }
137
138    /// Gets the owners of the Safe
139    pub async fn owners(&self) -> Result<Vec<Address>> {
140        let safe = ISafe::new(self.address, &self.provider);
141        let owners = safe
142            .getOwners()
143            .call()
144            .await
145            .map_err(|e| Error::Fetch {
146                what: "owners",
147                reason: e.to_string(),
148            })?;
149        Ok(owners)
150    }
151
152    /// Checks if an address is an owner of the Safe
153    pub async fn is_owner(&self, address: Address) -> Result<bool> {
154        let safe = ISafe::new(self.address, &self.provider);
155        let is_owner = safe
156            .isOwner(address)
157            .call()
158            .await
159            .map_err(|e| Error::Fetch {
160                what: "is_owner",
161                reason: e.to_string(),
162            })?;
163        Ok(is_owner)
164    }
165
166    /// Verifies that the signer is an owner and threshold is 1
167    pub async fn verify_single_owner(&self) -> Result<()> {
168        let threshold = self.threshold().await?;
169        if threshold != 1 {
170            return Err(Error::InvalidThreshold { threshold });
171        }
172
173        let is_owner = self.is_owner(self.signer.address()).await?;
174        if !is_owner {
175            return Err(Error::NotOwner {
176                signer: self.signer.address(),
177                safe: self.address,
178            });
179        }
180
181        Ok(())
182    }
183}
184
185/// Builder for constructing multicall transactions
186pub struct SafeBuilder<'a, P> {
187    safe: &'a Safe<P>,
188    calls: Vec<Call>,
189    use_call_only: bool,
190    safe_tx_gas: Option<U256>,
191    operation: Operation,
192    simulation_result: Option<SimulationResult>,
193}
194
195impl<'a, P> SafeBuilder<'a, P>
196where
197    P: Provider<AnyNetwork> + Clone + 'static,
198{
199    fn new(safe: &'a Safe<P>) -> Self {
200        SafeBuilder {
201            safe,
202            calls: Vec::new(),
203            use_call_only: false,
204            safe_tx_gas: None,
205            operation: Operation::DelegateCall, // MultiSend is called via delegatecall
206            simulation_result: None,
207        }
208    }
209
210    /// Use MultiSendCallOnly instead of MultiSend (no delegatecall allowed)
211    pub fn call_only(mut self) -> Self {
212        self.use_call_only = true;
213        self
214    }
215
216    /// Sets the operation type for the outer call (usually DelegateCall for MultiSend)
217    pub fn with_operation(mut self, operation: Operation) -> Self {
218        self.operation = operation;
219        self
220    }
221
222    /// Manually sets the safeTxGas instead of auto-estimating
223    pub fn with_safe_tx_gas(mut self, gas: U256) -> Self {
224        self.safe_tx_gas = Some(gas);
225        self
226    }
227
228    /// Sets the top-level `safe_tx_gas` for the entire Safe transaction.
229    ///
230    /// This is equivalent to `with_safe_tx_gas(U256::from(gas_limit))`.
231    pub fn with_gas_limit(mut self, gas_limit: u64) -> Self {
232        self.safe_tx_gas = Some(U256::from(gas_limit));
233        self
234    }
235
236    /// Simulates the multicall and stores the result
237    ///
238    /// This method does not return an error if the simulation reverts. Instead,
239    /// the result (success or failure) is stored internally. Use `simulation_success()`
240    /// to check if the simulation succeeded before calling `execute()`.
241    ///
242    /// After simulation, you can inspect the results via `simulation_result()`
243    /// and then call `execute()` which will use the simulation gas.
244    pub async fn simulate(mut self) -> Result<Self> {
245        if self.calls.is_empty() {
246            return Err(Error::NoCalls);
247        }
248
249        let (to, value, data, operation) = self.build_call_params()?;
250
251        let mut simulator = ForkSimulator::new(self.safe.provider.clone(), self.safe.config.chain_id);
252
253        // Configure debug output if the Safe has a debug output directory
254        if let Some(dir) = &self.safe.debug_output_dir {
255            simulator = simulator.with_debug_output_dir(dir.clone(), self.safe.address);
256        }
257
258        // For DelegateCall operations (like MultiSend), we need to simulate through
259        // Safe's execTransaction because the target contract expects delegatecall context.
260        // For regular Call operations, we can simulate the inner call directly.
261        let result = match operation {
262            Operation::DelegateCall => {
263                // Simulate through Safe.execTransaction
264                self.simulate_via_exec_transaction(&simulator, to, value, data, operation)
265                    .await?
266            }
267            Operation::Call => {
268                simulator
269                    .simulate_call(self.safe.address, to, value, data, operation)
270                    .await?
271            }
272        };
273
274        // Store the result regardless of success/failure
275        self.simulation_result = Some(result);
276        Ok(self)
277    }
278
279    /// Checks that simulation was performed and succeeded.
280    ///
281    /// Returns `Ok(self)` if simulation was performed and all calls succeeded.
282    /// Returns `Err(Error::SimulationNotPerformed)` if `simulate()` was not called.
283    /// Returns `Err(Error::SimulationReverted { reason })` if simulation failed.
284    ///
285    /// This is useful for chaining to ensure reverting transactions are not submitted:
286    /// ```ignore
287    /// safe.batch()
288    ///     .add_typed(target, call)
289    ///     .simulate().await?
290    ///     .simulation_success()?
291    ///     .execute().await?
292    /// ```
293    pub fn simulation_success(self) -> Result<Self> {
294        match &self.simulation_result {
295            None => Err(Error::SimulationNotPerformed),
296            Some(result) if !result.success => Err(Error::SimulationReverted {
297                reason: result
298                    .revert_reason
299                    .clone()
300                    .unwrap_or_else(|| "Unknown".to_string()),
301            }),
302            Some(_) => Ok(self),
303        }
304    }
305
306    /// Simulates by calling Safe.execTransaction
307    ///
308    /// This is needed for DelegateCall operations because the target contract
309    /// (like MultiSend) expects to be called via delegatecall.
310    async fn simulate_via_exec_transaction(
311        &self,
312        simulator: &ForkSimulator<P>,
313        to: Address,
314        value: U256,
315        data: Bytes,
316        operation: Operation,
317    ) -> Result<SimulationResult> {
318        // Get nonce
319        let nonce = self.safe.nonce().await?;
320
321        // Use a high gas estimate for simulation - we'll refine it after
322        let safe_tx_gas = U256::from(10_000_000);
323
324        // Build SafeTxParams
325        let params = SafeTxParams {
326            to,
327            value,
328            data: data.clone(),
329            operation,
330            safe_tx_gas,
331            base_gas: U256::ZERO,
332            gas_price: U256::ZERO,
333            gas_token: Address::ZERO,
334            refund_receiver: Address::ZERO,
335            nonce,
336        };
337
338        // Compute transaction hash
339        let tx_hash = compute_safe_transaction_hash(
340            self.safe.config.chain_id,
341            self.safe.address,
342            &params,
343        );
344
345        // Sign the hash
346        let signature = sign_hash(&self.safe.signer, tx_hash).await?;
347
348        // Build the execTransaction call
349        let exec_call = ISafe::execTransactionCall {
350            to: params.to,
351            value: params.value,
352            data: params.data,
353            operation: params.operation.as_u8(),
354            safeTxGas: params.safe_tx_gas,
355            baseGas: params.base_gas,
356            gasPrice: params.gas_price,
357            gasToken: params.gas_token,
358            refundReceiver: params.refund_receiver,
359            signatures: signature,
360        };
361
362        let exec_data = Bytes::from(exec_call.abi_encode());
363
364        // Simulate the execTransaction call
365        simulator
366            .simulate_call(
367                self.safe.signer.address(), // EOA calls Safe
368                self.safe.address,           // Safe address
369                U256::ZERO,                  // No ETH value for outer call
370                exec_data,
371                Operation::Call,             // Regular call to Safe
372            )
373            .await
374    }
375
376    /// Returns the simulation result if simulation was performed
377    pub fn simulation_result(&self) -> Option<&SimulationResult> {
378        self.simulation_result.as_ref()
379    }
380
381    /// Executes the multicall transaction
382    ///
383    /// If simulation was performed, uses the simulated gas + 10% buffer.
384    /// If no simulation, estimates gas via `eth_estimateGas` RPC call.
385    /// If `with_safe_tx_gas()` was called, uses that value instead.
386    pub async fn execute(self) -> Result<ExecutionResult> {
387        if self.calls.is_empty() {
388            return Err(Error::NoCalls);
389        }
390
391        let (to, value, data, operation) = self.build_call_params()?;
392
393        // Get nonce
394        let nonce = self.safe.nonce().await?;
395
396        // Determine safe_tx_gas: explicit > simulation > estimate
397        let safe_tx_gas = match (&self.simulation_result, self.safe_tx_gas) {
398            (_, Some(gas)) => gas, // User provided explicit gas
399            (Some(sim), None) => {
400                // Use simulation result + 10% buffer
401                let gas_used = sim.gas_used;
402                U256::from(gas_used + gas_used / 10)
403            }
404            (None, None) => {
405                // Estimate gas via RPC
406                use alloy::network::TransactionBuilder;
407                let tx_request = <AnyNetwork as alloy::network::Network>::TransactionRequest::default()
408                    .with_from(self.safe.address)
409                    .with_to(to)
410                    .with_value(value)
411                    .with_input(data.clone());
412
413                let estimated = self
414                    .safe
415                    .provider
416                    .estimate_gas(tx_request)
417                    .await
418                    .map_err(|e| Error::Provider(format!("gas estimation failed: {}", e)))?;
419
420                // Add 10% buffer
421                U256::from(estimated + estimated / 10)
422            }
423        };
424
425        // Build SafeTxParams
426        let params = SafeTxParams {
427            to,
428            value,
429            data: data.clone(),
430            operation,
431            safe_tx_gas,
432            base_gas: U256::ZERO,
433            gas_price: U256::ZERO,
434            gas_token: Address::ZERO,
435            refund_receiver: Address::ZERO,
436            nonce,
437        };
438
439        // Compute transaction hash
440        let tx_hash = compute_safe_transaction_hash(
441            self.safe.config.chain_id,
442            self.safe.address,
443            &params,
444        );
445
446        // Sign the hash
447        let signature = sign_hash(&self.safe.signer, tx_hash).await?;
448
449        // Build the execTransaction call
450        let exec_call = ISafe::execTransactionCall {
451            to: params.to,
452            value: params.value,
453            data: params.data,
454            operation: params.operation.as_u8(),
455            safeTxGas: params.safe_tx_gas,
456            baseGas: params.base_gas,
457            gasPrice: params.gas_price,
458            gasToken: params.gas_token,
459            refundReceiver: params.refund_receiver,
460            signatures: signature,
461        };
462
463        // Execute the transaction through the provider
464        let safe_contract = ISafe::new(self.safe.address, &self.safe.provider);
465
466        let builder = safe_contract.execTransaction(
467            exec_call.to,
468            exec_call.value,
469            exec_call.data,
470            exec_call.operation,
471            exec_call.safeTxGas,
472            exec_call.baseGas,
473            exec_call.gasPrice,
474            exec_call.gasToken,
475            exec_call.refundReceiver,
476            exec_call.signatures,
477        );
478
479        let pending_tx = builder
480            .send()
481            .await
482            .map_err(|e| Error::ExecutionFailed {
483                reason: e.to_string(),
484            })?;
485
486        let receipt = pending_tx
487            .get_receipt()
488            .await
489            .map_err(|e| Error::ExecutionFailed {
490                reason: e.to_string(),
491            })?;
492
493        // Check if Safe execution succeeded
494        let success = receipt.status();
495
496        Ok(ExecutionResult {
497            tx_hash: receipt.transaction_hash,
498            success,
499        })
500    }
501
502    fn build_call_params(&self) -> Result<(Address, U256, Bytes, Operation)> {
503        if self.calls.len() == 1 {
504            // Single call - execute directly
505            let call = &self.calls[0];
506            Ok((call.to, call.value, call.data.clone(), Operation::Call))
507        } else {
508            // Multiple calls - use MultiSend
509            let multisend_data = encode_multisend_data(&self.calls);
510
511            let (multisend_address, calldata) = if self.use_call_only {
512                let call = IMultiSendCallOnly::multiSendCall {
513                    transactions: multisend_data,
514                };
515                (
516                    self.safe.addresses().multi_send_call_only,
517                    Bytes::from(call.abi_encode()),
518                )
519            } else {
520                let call = IMultiSend::multiSendCall {
521                    transactions: multisend_data,
522                };
523                (
524                    self.safe.addresses().multi_send,
525                    Bytes::from(call.abi_encode()),
526                )
527            };
528
529            // MultiSend is called with zero value; individual call values are encoded in the data
530            Ok((multisend_address, U256::ZERO, calldata, Operation::DelegateCall))
531        }
532    }
533}
534
535impl<P> CallBuilder for SafeBuilder<'_, P>
536where
537    P: Provider<AnyNetwork> + Clone + Send + Sync + 'static,
538{
539    fn calls_mut(&mut self) -> &mut Vec<Call> {
540        &mut self.calls
541    }
542
543    fn calls(&self) -> &Vec<Call> {
544        &self.calls
545    }
546
547    fn with_gas_limit(self, gas_limit: u64) -> Self {
548        SafeBuilder::with_gas_limit(self, gas_limit)
549    }
550
551    async fn simulate(self) -> Result<Self> {
552        SafeBuilder::simulate(self).await
553    }
554
555    fn simulation_result(&self) -> Option<&SimulationResult> {
556        self.simulation_result.as_ref()
557    }
558
559    fn simulation_success(self) -> Result<Self> {
560        SafeBuilder::simulation_success(self)
561    }
562}
563
564impl<P> crate::account::Account for Safe<P>
565where
566    P: Provider<AnyNetwork> + Clone + Send + Sync + 'static,
567{
568    type Provider = P;
569    type Builder<'a> = SafeBuilder<'a, P> where Self: 'a;
570
571    fn address(&self) -> Address {
572        self.address
573    }
574
575    fn signer_address(&self) -> Address {
576        self.signer.address()
577    }
578
579    fn config(&self) -> &ChainConfig {
580        &self.config
581    }
582
583    fn provider(&self) -> &P {
584        &self.provider
585    }
586
587    fn debug_output_dir(&self) -> Option<&Path> {
588        self.debug_output_dir.as_deref()
589    }
590
591    async fn nonce(&self) -> Result<U256> {
592        let safe = ISafe::new(self.address, &self.provider);
593        let nonce = safe
594            .nonce()
595            .call()
596            .await
597            .map_err(|e| Error::Fetch {
598                what: "nonce",
599                reason: e.to_string(),
600            })?;
601        Ok(nonce)
602    }
603
604    fn batch(&self) -> SafeBuilder<'_, P> {
605        SafeBuilder::new(self)
606    }
607
608    async fn execute_single(
609        &self,
610        to: Address,
611        value: U256,
612        data: Bytes,
613        operation: Operation,
614    ) -> Result<ExecutionResult> {
615        self.batch()
616            .add_raw(to, value, data)
617            .with_operation(operation)
618            .simulate()
619            .await?
620            .simulation_success()?
621            .execute()
622            .await
623    }
624}
625
626#[cfg(test)]
627mod tests {
628    #[allow(unused_imports)]
629    use super::*;
630    use alloy::primitives::address;
631
632    #[test]
633    fn test_call_params_single() {
634        // This would need a mock provider to test fully
635        // For now, just test that types compile correctly
636        let _addr = address!("0x1234567890123456789012345678901234567890");
637    }
638
639    #[test]
640    fn test_safe_singleton_slot_is_zero() {
641        assert_eq!(SAFE_SINGLETON_SLOT, U256::ZERO);
642    }
643}