Skip to main content

cast/cmd/safe/
proposal.rs

1use super::{
2    SafeOperation,
3    contracts::ISafe,
4    rpc_provider,
5    service::{SafeServiceOpts, SafeTransaction},
6    signing::sign_safe_hash,
7};
8use alloy_primitives::{Address, B256, Bytes, U256};
9use alloy_signer::Signer;
10use clap::Args;
11use eyre::Result;
12use foundry_cli::{json::print_scalar, opts::RpcOpts, utils::parse_ether_value};
13use foundry_common::abi::{encode_function_args, get_func};
14use foundry_wallets::WalletOpts;
15use reqwest::Method;
16use serde_json::json;
17
18/// CLI arguments for `cast safe propose`.
19#[derive(Args, Debug)]
20pub struct ProposeArgs {
21    /// Safe account address.
22    safe: Address,
23
24    /// Transaction target.
25    to: Address,
26
27    /// Function signature to call.
28    sig: Option<String>,
29
30    /// Function arguments.
31    #[arg(allow_negative_numbers = true)]
32    args: Vec<String>,
33
34    /// Raw calldata. Cannot be combined with a function signature or arguments.
35    #[arg(long, conflicts_with_all = ["sig", "args"])]
36    data: Option<Bytes>,
37
38    /// Native token value sent by the Safe.
39    #[arg(long, default_value = "0", value_parser = parse_ether_value)]
40    value: U256,
41
42    /// Safe operation type.
43    #[arg(long, value_enum, default_value_t = SafeOperation::Call)]
44    operation: SafeOperation,
45
46    /// Safe transaction gas.
47    #[arg(long, default_value = "0")]
48    safe_tx_gas: U256,
49
50    /// Base gas reimbursed by the Safe.
51    #[arg(long, default_value = "0")]
52    base_gas: U256,
53
54    /// Gas price reimbursed by the Safe.
55    #[arg(long, default_value = "0")]
56    gas_price: U256,
57
58    /// Token used for gas reimbursement.
59    #[arg(long, default_value_t = Address::ZERO)]
60    gas_token: Address,
61
62    /// Gas reimbursement receiver.
63    #[arg(long, default_value_t = Address::ZERO)]
64    refund_receiver: Address,
65
66    /// Safe nonce. Defaults to the next queued Transaction Service nonce.
67    #[arg(long)]
68    nonce: Option<U256>,
69
70    /// Optional origin shown by Safe clients.
71    #[arg(long)]
72    origin: Option<String>,
73
74    #[command(flatten)]
75    service: Box<SafeServiceOpts>,
76
77    #[command(flatten)]
78    rpc: Box<RpcOpts>,
79
80    #[command(flatten)]
81    wallet: Box<WalletOpts>,
82}
83
84impl ProposeArgs {
85    pub(super) async fn run(self) -> Result<()> {
86        let Self {
87            safe,
88            to,
89            sig,
90            args,
91            data,
92            value,
93            operation,
94            safe_tx_gas,
95            base_gas,
96            gas_price,
97            gas_token,
98            refund_receiver,
99            nonce,
100            origin,
101            service,
102            rpc,
103            wallet,
104        } = self;
105        let data = match (data, sig) {
106            (Some(data), None) => data,
107            (None, Some(sig)) => encode_function_args(&get_func(&sig)?, &args)?.into(),
108            (None, None) => Bytes::new(),
109            (Some(_), Some(_)) => unreachable!("enforced by clap"),
110        };
111        let (provider, chain_id) = rpc_provider(&rpc).await?;
112        let nonce = match nonce {
113            Some(nonce) => nonce,
114            None => {
115                let onchain_nonce = ISafe::new(safe, &provider).nonce().call().await?;
116                service.next_nonce(chain_id, safe, onchain_nonce).await?
117            }
118        };
119        let mut transaction = SafeTransaction {
120            safe,
121            to,
122            value: value.to_string(),
123            data,
124            operation: operation as u8,
125            safe_tx_gas: safe_tx_gas.to_string(),
126            base_gas: base_gas.to_string(),
127            gas_price: gas_price.to_string(),
128            gas_token,
129            refund_receiver,
130            nonce: nonce.to_string(),
131            safe_tx_hash: B256::ZERO,
132            confirmations: Vec::new(),
133            is_executed: false,
134            transaction_hash: None,
135        };
136        transaction.safe_tx_hash = transaction.calculate_hash(&provider).await?;
137        transaction.show_transaction_summary()?;
138        let signer = wallet.signer().await?;
139        let signature = sign_safe_hash(&signer, transaction.safe_tx_hash).await?;
140        let url = service.endpoint(
141            chain_id,
142            &format!("v2/safes/{}/multisig-transactions/", transaction.safe.to_checksum(None)),
143        )?;
144        let body = transaction.proposal_body(signer.address(), signature, origin);
145        service.empty_response(service.request(Method::POST, url).json(&body)).await?;
146        print_scalar(transaction.safe_tx_hash)
147    }
148}
149
150/// CLI arguments for `cast safe sign`.
151#[derive(Args, Debug)]
152pub struct SignArgs {
153    /// Safe account address.
154    safe: Address,
155
156    /// Safe transaction hash from the Transaction Service.
157    safe_tx_hash: B256,
158
159    #[command(flatten)]
160    service: Box<SafeServiceOpts>,
161
162    #[command(flatten)]
163    rpc: Box<RpcOpts>,
164
165    #[command(flatten)]
166    wallet: Box<WalletOpts>,
167}
168
169impl SignArgs {
170    pub(super) async fn run(self) -> Result<()> {
171        let Self { safe, safe_tx_hash, service, rpc, wallet } = self;
172        let (provider, chain_id) = rpc_provider(&rpc).await?;
173        let transaction = service.get_transaction(chain_id, "v1", safe_tx_hash).await?;
174        transaction.verify_hash(safe, &provider).await?;
175        transaction.show_transaction_summary()?;
176        let signature = sign_safe_hash(&wallet.signer().await?, safe_tx_hash).await?;
177        let url = service.endpoint(
178            chain_id,
179            &format!("v1/multisig-transactions/{safe_tx_hash}/confirmations/"),
180        )?;
181        let body = json!({ "signature": signature });
182        service.empty_response(service.request(Method::POST, url).json(&body)).await?;
183        print_scalar(signature)
184    }
185}