Skip to main content

safe_rs/
safe.rs

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