Skip to main content

libid_contracts/
deploy.rs

1//! Generic deploy and upgrade primitives, usable over any alloy
2//! [`Provider`] that has a wallet wired in. Signing is the consumer's
3//! concern; nothing here constructs or holds keys.
4
5use alloy::{
6    network::TransactionBuilder,
7    primitives::{
8        Address,
9        Bytes,
10        B256,
11    },
12    providers::Provider,
13    rpc::types::TransactionRequest,
14    sol_types::SolCall,
15};
16
17use crate::{
18    artifacts::Artifacts,
19    bindings::proxy::IUUPSUpgradeable,
20    error::{
21        Error,
22        Result,
23    },
24    factory::CREATE2_DEPLOYER,
25};
26
27/// Send a contract call with automatic retry on "nonce too low" errors.
28///
29/// Alloy's nonce manager can get stale when earlier calls fail at dry-run
30/// (e.g. a call reverting "already applied"). On each attempt the real nonce
31/// is fetched from the chain and set explicitly, bypassing the cached nonce
32/// manager entirely.
33///
34/// Usage:
35/// `send_with_nonce_retry!(contract.doSomething(args), "label", provider, sender)?;`
36#[macro_export]
37macro_rules! send_with_nonce_retry {
38    ($call_expr:expr, $label:expr, $provider:expr, $sender:expr) => {{
39        const MAX_RETRIES: u32 = 3;
40        let mut result: $crate::Result<alloy::rpc::types::TransactionReceipt> =
41            Err($crate::Error::Rpc {
42                detail: "unreachable".into(),
43            });
44        for attempt in 0..MAX_RETRIES {
45            let nonce =
46                alloy::providers::Provider::get_transaction_count($provider, $sender)
47                    .await
48                    .map_err(|e| $crate::Error::Rpc {
49                        detail: format!("{} failed to fetch nonce: {e}", $label),
50                    })?;
51            match ($call_expr).nonce(nonce).send().await {
52                Ok(pending) => {
53                    result =
54                        pending.get_receipt().await.map_err(|e| $crate::Error::Rpc {
55                            detail: format!("{} confirmation failed: {e}", $label),
56                        });
57                    break;
58                }
59                Err(e) => {
60                    let msg = e.to_string();
61                    let next_attempt = attempt.saturating_add(1);
62                    if msg.contains("nonce too low") && next_attempt < MAX_RETRIES {
63                        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
64                        continue;
65                    }
66                    result = Err($crate::Error::Rpc {
67                        detail: format!("{} send failed: {e}", $label),
68                    });
69                    break;
70                }
71            }
72        }
73        result
74    }};
75}
76
77/// Deploy a contract and return its address.
78pub async fn deploy_contract<P: Provider>(
79    provider: &P,
80    bytecode: Bytes,
81    label: &str,
82) -> Result<Address> {
83    deploy_contract_from(provider, bytecode, label, None).await
84}
85
86/// Deploy a contract, optionally fetching the sender's nonce explicitly.
87///
88/// Pass `sender` when mixing provider-managed and manually-nonce'd
89/// transactions in one run: the cached nonce manager goes stale otherwise.
90pub async fn deploy_contract_from<P: Provider>(
91    provider: &P,
92    bytecode: Bytes,
93    label: &str,
94    sender: Option<Address>,
95) -> Result<Address> {
96    let mut tx = TransactionRequest::default().with_deploy_code(bytecode);
97
98    if let Some(addr) = sender {
99        let nonce =
100            provider
101                .get_transaction_count(addr)
102                .await
103                .map_err(|e| Error::Rpc {
104                    detail: format!("failed to fetch nonce for {label}: {e}"),
105                })?;
106        tx = tx.with_nonce(nonce);
107    }
108
109    let pending = provider
110        .send_transaction(tx)
111        .await
112        .map_err(|e| Error::Rpc {
113            detail: format!("failed to send {label} deploy tx: {e}"),
114        })?;
115
116    let receipt = pending.get_receipt().await.map_err(|e| Error::Rpc {
117        detail: format!("failed to get {label} deploy receipt: {e}"),
118    })?;
119
120    receipt.contract_address.ok_or_else(|| Error::Rpc {
121        detail: format!("{label} deploy did not return contract address"),
122    })
123}
124
125/// Deploy `bytecode` with ABI-encoded constructor args appended.
126pub async fn deploy_with_ctor<P: Provider>(
127    provider: &P,
128    bytecode: &Bytes,
129    constructor_args: &[u8],
130    label: &str,
131    sender: Option<Address>,
132) -> Result<Address> {
133    let mut deploy_bytecode = bytecode.to_vec();
134    deploy_bytecode.extend_from_slice(constructor_args);
135    deploy_contract_from(provider, Bytes::from(deploy_bytecode), label, sender).await
136}
137
138/// Deploy an ERC1967 proxy pointing at `implementation` with `init_data` (the
139/// ABI-encoded initializer call).
140pub async fn deploy_proxy<P: Provider>(
141    provider: &P,
142    proxy_bytecode: &Bytes,
143    implementation: Address,
144    init_data: Bytes,
145    label: &str,
146    sender: Option<Address>,
147) -> Result<Address> {
148    // ERC1967Proxy constructor: (address implementation, bytes memory _data)
149    let constructor_args =
150        alloy::sol_types::SolValue::abi_encode_params(&(implementation, init_data));
151    deploy_with_ctor(provider, proxy_bytecode, &constructor_args, label, sender).await
152}
153
154/// Deploy an implementation from `artifacts` and put it behind a fresh
155/// ERC1967 proxy whose init data is `init_call` ABI-encoded. Returns the
156/// proxy address. The common shape of every UUPS deploy in the stack.
157pub async fn deploy_behind_proxy<P: Provider, C: SolCall>(
158    provider: &P,
159    artifacts: &Artifacts,
160    contract: &str,
161    init_call: &C,
162    sender: Option<Address>,
163) -> Result<Address> {
164    let implementation = deploy_contract_from(
165        provider,
166        artifacts.bytecode(contract)?,
167        &format!("{contract} (impl)"),
168        sender,
169    )
170    .await?;
171    let proxy_bytecode = artifacts.bytecode("ERC1967Proxy")?;
172    deploy_proxy(
173        provider,
174        &proxy_bytecode,
175        implementation,
176        init_call.abi_encode().into(),
177        &format!("{contract} (proxy)"),
178        sender,
179    )
180    .await
181}
182
183/// Upgrade a UUPS proxy: deploy `contract`'s current implementation from
184/// `artifacts`, then call `upgradeToAndCall(new_impl, data)` on the proxy.
185/// Returns the new implementation address. `data` is usually empty (state is
186/// already initialized); pass a re-initializer call when the upgrade needs
187/// one.
188pub async fn upgrade_uups<P: Provider>(
189    provider: &P,
190    artifacts: &Artifacts,
191    proxy: Address,
192    contract: &str,
193    data: Bytes,
194    sender: Option<Address>,
195) -> Result<Address> {
196    let new_impl = deploy_contract_from(
197        provider,
198        artifacts.bytecode(contract)?,
199        &format!("{contract} (new impl)"),
200        sender,
201    )
202    .await?;
203    let proxied = IUUPSUpgradeable::new(proxy, provider);
204    let call = proxied.upgradeToAndCall(new_impl, data);
205    let pending =
206        match sender {
207            Some(addr) => {
208                let nonce = provider.get_transaction_count(addr).await.map_err(|e| {
209                    Error::Rpc {
210                        detail: format!("{contract} upgrade failed to fetch nonce: {e}"),
211                    }
212                })?;
213                call.nonce(nonce).send().await
214            }
215            None => call.send().await,
216        }
217        .map_err(|e| Error::Rpc {
218            detail: format!("{contract} upgradeToAndCall send failed: {e}"),
219        })?;
220    pending.get_receipt().await.map_err(|e| Error::Rpc {
221        detail: format!("{contract} upgradeToAndCall confirmation failed: {e}"),
222    })?;
223    Ok(new_impl)
224}
225
226/// Deploy `salt ++ init_code` through the canonical CREATE2 deployer and
227/// check that code landed at `predicted`.
228///
229/// `sender` opts into explicit nonce management (see
230/// [`deploy_contract_from`]).
231pub(crate) async fn deploy_via_create2<P: Provider>(
232    provider: &P,
233    salt: B256,
234    init_code: &[u8],
235    predicted: Address,
236    label: &str,
237    sender: Option<Address>,
238) -> Result<()> {
239    let mut input = salt.to_vec();
240    input.extend_from_slice(init_code);
241    let mut tx = TransactionRequest::default()
242        .with_to(CREATE2_DEPLOYER)
243        .with_input(Bytes::from(input));
244    if let Some(addr) = sender {
245        let nonce =
246            provider
247                .get_transaction_count(addr)
248                .await
249                .map_err(|e| Error::Rpc {
250                    detail: format!("{label}: failed to fetch nonce: {e}"),
251                })?;
252        tx = tx.with_nonce(nonce);
253    }
254    let pending = provider
255        .send_transaction(tx)
256        .await
257        .map_err(|e| Error::Rpc {
258            detail: format!("{label}: CREATE2 deploy send failed: {e}"),
259        })?;
260    pending.get_receipt().await.map_err(|e| Error::Rpc {
261        detail: format!("{label}: CREATE2 deploy confirmation failed: {e}"),
262    })?;
263    if !code_present(provider, predicted, label).await? {
264        return Err(Error::Rpc {
265            detail: format!("{label}: no code at the predicted address {predicted}"),
266        });
267    }
268    Ok(())
269}
270
271async fn code_present<P: Provider>(
272    provider: &P,
273    address: Address,
274    label: &str,
275) -> Result<bool> {
276    let code = provider
277        .get_code_at(address)
278        .await
279        .map_err(|e| Error::Rpc {
280            detail: format!("{label}: failed to read code at {address}: {e}"),
281        })?;
282    Ok(!code.is_empty())
283}