Skip to main content

cast/cmd/safe/
deploy.rs

1use super::{
2    contracts::{
3        ISafe, ISafeProxyFactory, PREDETERMINED_SALT_NONCE, SAFE_L2_V1_4_1, SAFE_V1_4_1,
4        SENTINEL_OWNER,
5    },
6    transaction::send_safe_call,
7};
8use alloy_network::Ethereum;
9use alloy_primitives::{Address, Bytes, U256, keccak256, map::AddressHashSet};
10use alloy_provider::Provider;
11use alloy_sol_types::{SolCall, SolEvent};
12use eyre::{Context, Result, ensure};
13use foundry_cli::{
14    json::print_scalar,
15    opts::{RpcOpts, TransactionOpts},
16    utils::LoadConfig,
17};
18use foundry_common::{provider::ProviderBuilder, sh_status};
19use foundry_wallets::WalletOpts;
20
21#[allow(clippy::too_many_arguments)]
22pub(super) async fn run(
23    owners: Vec<Address>,
24    threshold: Option<usize>,
25    salt_nonce: Option<U256>,
26    singleton: Option<Address>,
27    l1: bool,
28    factory: Address,
29    fallback_handler: Address,
30    confirmations: u64,
31    timeout: Option<u64>,
32    poll_interval: Option<u64>,
33    rpc: RpcOpts,
34    wallet: WalletOpts,
35    tx: TransactionOpts,
36) -> Result<()> {
37    let threshold = validate_owners(&owners, threshold)?;
38    let config = rpc.load_config()?;
39    let provider = ProviderBuilder::<Ethereum>::from_config(&config)?.build()?;
40    let chain_id = provider.get_chain_id().await?;
41    let singleton =
42        singleton.unwrap_or(if l1 || chain_id == 1 { SAFE_V1_4_1 } else { SAFE_L2_V1_4_1 });
43    ensure_contract(&provider, singleton, "Safe singleton", "--singleton").await?;
44    ensure_contract(&provider, factory, "SafeProxyFactory", "--factory").await?;
45    if fallback_handler != Address::ZERO {
46        ensure_contract(
47            &provider,
48            fallback_handler,
49            "CompatibilityFallbackHandler",
50            "--fallback-handler",
51        )
52        .await?;
53    }
54
55    let salt_nonce = salt_nonce.unwrap_or_else(|| default_salt_nonce(chain_id));
56    let initializer: Bytes = ISafe::setupCall {
57        owners,
58        threshold: U256::from(threshold),
59        to: Address::ZERO,
60        data: Bytes::new(),
61        fallbackHandler: fallback_handler,
62        paymentToken: Address::ZERO,
63        payment: U256::ZERO,
64        paymentReceiver: Address::ZERO,
65    }
66    .abi_encode()
67    .into();
68
69    sh_status!("Deploying Safe with singleton {singleton}")?;
70    let calldata: Bytes = ISafeProxyFactory::createProxyWithNonceCall {
71        singleton,
72        initializer,
73        saltNonce: salt_nonce,
74    }
75    .abi_encode()
76    .into();
77    let result =
78        send_safe_call(factory, calldata, confirmations, timeout, poll_interval, rpc, wallet, tx)
79            .await
80            .wrap_err("failed to submit Safe deployment")?;
81    let deployed = result
82        .logs
83        .iter()
84        .filter(|log| log.address() == factory)
85        .find_map(|log| ISafeProxyFactory::ProxyCreation::decode_log(&log.inner).ok())
86        .ok_or_else(|| eyre::eyre!("Safe deployment receipt did not emit ProxyCreation"))?;
87    sh_status!("Transaction hash: {}", result.tx_hash)?;
88    print_scalar(deployed.proxy.to_checksum(None))?;
89    Ok(())
90}
91
92fn validate_owners(owners: &[Address], threshold: Option<usize>) -> Result<usize> {
93    ensure!(!owners.is_empty(), "at least one Safe owner is required");
94    let threshold = threshold.unwrap_or(owners.len());
95    ensure!(threshold > 0, "Safe threshold must be greater than zero");
96    ensure!(
97        threshold <= owners.len(),
98        "Safe threshold ({threshold}) exceeds owner count ({})",
99        owners.len()
100    );
101    let mut unique = AddressHashSet::default();
102    for owner in owners {
103        ensure!(*owner != Address::ZERO, "Safe owner cannot be the zero address");
104        ensure!(*owner != SENTINEL_OWNER, "Safe owner cannot be the sentinel address");
105        ensure!(unique.insert(*owner), "duplicate Safe owner: {owner}");
106    }
107    Ok(threshold)
108}
109
110fn default_salt_nonce(chain_id: u64) -> U256 {
111    U256::from_be_slice(keccak256(format!("{PREDETERMINED_SALT_NONCE}{chain_id}")).as_slice())
112}
113
114pub(super) async fn ensure_contract<P>(
115    provider: &P,
116    address: Address,
117    name: &str,
118    flag: &str,
119) -> Result<()>
120where
121    P: Provider<Ethereum>,
122{
123    ensure!(
124        !provider.get_code_at(address).await?.is_empty(),
125        "{name} is not deployed at {address}; provide the network deployment with {flag}"
126    );
127    Ok(())
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use std::str::FromStr;
134
135    #[test]
136    fn validates_safe_owner_configuration() {
137        let owners = [Address::repeat_byte(1), Address::repeat_byte(2)];
138        assert_eq!(validate_owners(&owners, None).unwrap(), 2);
139        assert_eq!(validate_owners(&owners, Some(1)).unwrap(), 1);
140        assert!(validate_owners(&owners, Some(3)).is_err());
141        assert!(validate_owners(&[owners[0], owners[0]], None).is_err());
142        assert!(validate_owners(&[Address::ZERO], None).is_err());
143    }
144
145    #[test]
146    fn matches_protocol_kit_default_salt_nonce() {
147        assert_eq!(
148            default_salt_nonce(1),
149            U256::from_str("0x69b348339eea4ed93f9d11931c3b894c8f9d8c7663a053024b11cb7eb4e5a1f6")
150                .unwrap()
151        );
152    }
153}