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    hex,
7    primitives::{
8        Address,
9        Bytes,
10    },
11    providers::Provider,
12    sol_types::SolCall,
13};
14
15use crate::{
16    artifacts::Artifacts,
17    bindings::proxy::IUUPSUpgradeable,
18    error::{
19        Error,
20        Result,
21    },
22};
23
24/// Send a contract call with automatic retry on "nonce too low" errors.
25///
26/// Alloy's nonce manager can get stale when earlier calls fail at dry-run
27/// (e.g. a call reverting "already applied"). On each attempt the real nonce
28/// is fetched from the chain and set explicitly, bypassing the cached nonce
29/// manager entirely.
30///
31/// Usage:
32/// `send_with_nonce_retry!(contract.doSomething(args), "label", provider, sender)?;`
33#[macro_export]
34macro_rules! send_with_nonce_retry {
35    ($call_expr:expr, $label:expr, $provider:expr, $sender:expr) => {{
36        const MAX_RETRIES: u32 = 3;
37        let mut result: $crate::Result<alloy::rpc::types::TransactionReceipt> =
38            Err($crate::Error::Rpc {
39                detail: "unreachable".into(),
40            });
41        for attempt in 0..MAX_RETRIES {
42            let nonce =
43                alloy::providers::Provider::get_transaction_count($provider, $sender)
44                    .await
45                    .map_err(|e| $crate::Error::Rpc {
46                        detail: format!("{} failed to fetch nonce: {e}", $label),
47                    })?;
48            match ($call_expr).nonce(nonce).send().await {
49                Ok(pending) => {
50                    result =
51                        pending.get_receipt().await.map_err(|e| $crate::Error::Rpc {
52                            detail: format!("{} confirmation failed: {e}", $label),
53                        });
54                    break;
55                }
56                Err(e) => {
57                    let msg = e.to_string();
58                    let next_attempt = attempt.saturating_add(1);
59                    if msg.contains("nonce too low") && next_attempt < MAX_RETRIES {
60                        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
61                        continue;
62                    }
63                    result = Err($crate::Error::Rpc {
64                        detail: format!("{} send failed: {e}", $label),
65                    });
66                    break;
67                }
68            }
69        }
70        result
71    }};
72}
73
74/// Deploy a contract and return its address.
75pub async fn deploy_contract<P: Provider>(
76    provider: &P,
77    bytecode: Bytes,
78    label: &str,
79) -> Result<Address> {
80    deploy_contract_from(provider, bytecode, label, None).await
81}
82
83/// Deploy a contract, optionally fetching the sender's nonce explicitly.
84///
85/// Pass `sender` when mixing provider-managed and manually-nonce'd
86/// transactions in one run: the cached nonce manager goes stale otherwise.
87pub async fn deploy_contract_from<P: Provider>(
88    provider: &P,
89    bytecode: Bytes,
90    label: &str,
91    sender: Option<Address>,
92) -> Result<Address> {
93    use alloy::{
94        network::TransactionBuilder,
95        rpc::types::TransactionRequest,
96    };
97
98    let mut tx = TransactionRequest::default().with_deploy_code(bytecode);
99
100    if let Some(addr) = sender {
101        let nonce =
102            provider
103                .get_transaction_count(addr)
104                .await
105                .map_err(|e| Error::Rpc {
106                    detail: format!("failed to fetch nonce for {label}: {e}"),
107                })?;
108        tx = tx.with_nonce(nonce);
109    }
110
111    let pending = provider
112        .send_transaction(tx)
113        .await
114        .map_err(|e| Error::Rpc {
115            detail: format!("failed to send {label} deploy tx: {e}"),
116        })?;
117
118    let receipt = pending.get_receipt().await.map_err(|e| Error::Rpc {
119        detail: format!("failed to get {label} deploy receipt: {e}"),
120    })?;
121
122    receipt.contract_address.ok_or_else(|| Error::Rpc {
123        detail: format!("{label} deploy did not return contract address"),
124    })
125}
126
127/// Deploy `bytecode` with ABI-encoded constructor args appended.
128pub async fn deploy_with_ctor<P: Provider>(
129    provider: &P,
130    bytecode: &Bytes,
131    constructor_args: &[u8],
132    label: &str,
133    sender: Option<Address>,
134) -> Result<Address> {
135    let mut deploy_bytecode = bytecode.to_vec();
136    deploy_bytecode.extend_from_slice(constructor_args);
137    deploy_contract_from(provider, Bytes::from(deploy_bytecode), label, sender).await
138}
139
140/// Deploy an ERC1967 proxy pointing at `implementation` with `init_data` (the
141/// ABI-encoded initializer call).
142pub async fn deploy_proxy<P: Provider>(
143    provider: &P,
144    proxy_bytecode: &Bytes,
145    implementation: Address,
146    init_data: Bytes,
147    label: &str,
148    sender: Option<Address>,
149) -> Result<Address> {
150    // ERC1967Proxy constructor: (address implementation, bytes memory _data)
151    let constructor_args =
152        alloy::sol_types::SolValue::abi_encode_params(&(implementation, init_data));
153    deploy_with_ctor(provider, proxy_bytecode, &constructor_args, label, sender).await
154}
155
156/// Deploy an implementation from `artifacts` and put it behind a fresh
157/// ERC1967 proxy whose init data is `init_call` ABI-encoded. Returns the
158/// proxy address. The common shape of every UUPS deploy in the stack.
159pub async fn deploy_behind_proxy<P: Provider, C: SolCall>(
160    provider: &P,
161    artifacts: &Artifacts,
162    contract: &str,
163    init_call: &C,
164    sender: Option<Address>,
165) -> Result<Address> {
166    let implementation = deploy_contract_from(
167        provider,
168        artifacts.bytecode(contract)?,
169        &format!("{contract} (impl)"),
170        sender,
171    )
172    .await?;
173    let proxy_bytecode = artifacts.bytecode("ERC1967Proxy")?;
174    deploy_proxy(
175        provider,
176        &proxy_bytecode,
177        implementation,
178        init_call.abi_encode().into(),
179        &format!("{contract} (proxy)"),
180        sender,
181    )
182    .await
183}
184
185/// Upgrade a UUPS proxy: deploy `contract`'s current implementation from
186/// `artifacts`, then call `upgradeToAndCall(new_impl, data)` on the proxy.
187/// Returns the new implementation address. `data` is usually empty (state is
188/// already initialized); pass a re-initializer call when the upgrade needs
189/// one.
190pub async fn upgrade_uups<P: Provider>(
191    provider: &P,
192    artifacts: &Artifacts,
193    proxy: Address,
194    contract: &str,
195    data: Bytes,
196    sender: Option<Address>,
197) -> Result<Address> {
198    let new_impl = deploy_contract_from(
199        provider,
200        artifacts.bytecode(contract)?,
201        &format!("{contract} (new impl)"),
202        sender,
203    )
204    .await?;
205    let proxied = IUUPSUpgradeable::new(proxy, provider);
206    let call = proxied.upgradeToAndCall(new_impl, data);
207    let pending =
208        match sender {
209            Some(addr) => {
210                let nonce = provider.get_transaction_count(addr).await.map_err(|e| {
211                    Error::Rpc {
212                        detail: format!("{contract} upgrade failed to fetch nonce: {e}"),
213                    }
214                })?;
215                call.nonce(nonce).send().await
216            }
217            None => call.send().await,
218        }
219        .map_err(|e| Error::Rpc {
220            detail: format!("{contract} upgradeToAndCall send failed: {e}"),
221        })?;
222    pending.get_receipt().await.map_err(|e| Error::Rpc {
223        detail: format!("{contract} upgradeToAndCall confirmation failed: {e}"),
224    })?;
225    Ok(new_impl)
226}
227
228/// Load a contract's creation bytecode, deploying and linking any external
229/// libraries it references (the bb-generated UltraHonk verifiers link
230/// `ZKTranscriptLib`). Mirrors what `forge` does automatically. For artifacts
231/// with no `linkReferences` this behaves like [`Artifacts::bytecode_named`]
232/// and sends nothing.
233pub async fn load_linked_bytecode<P: Provider>(
234    provider: &P,
235    artifacts: &Artifacts,
236    file: &str,
237    contract: &str,
238    sender: Option<Address>,
239) -> Result<Bytes> {
240    let mut hex_str = artifacts.bytecode_hex(file, contract)?;
241
242    // Resolve each linked library: deploy it (recursively linking its own
243    // deps) then substitute its address into the placeholder slots.
244    for (lib_file, libs) in artifacts.link_references(file, contract)? {
245        let lib_stem = std::path::Path::new(&lib_file)
246            .file_stem()
247            .and_then(|s| s.to_str())
248            .ok_or_else(|| Error::Artifact {
249                detail: format!("bad library file path {lib_file}"),
250            })?;
251        for (lib_name, refs) in libs.as_object().into_iter().flatten() {
252            let lib_bytecode = Box::pin(load_linked_bytecode(
253                provider, artifacts, lib_stem, lib_name, sender,
254            ))
255            .await?;
256            let lib_addr = deploy_contract_from(
257                provider,
258                lib_bytecode,
259                &format!("{lib_name} (library)"),
260                sender,
261            )
262            .await?;
263            let addr_hex = hex::encode(lib_addr.as_slice()); // 40 hex chars
264            for r in refs.as_array().into_iter().flatten() {
265                let start = r["start"]
266                    .as_u64()
267                    .and_then(|v| usize::try_from(v).ok())
268                    .ok_or_else(|| Error::Artifact {
269                        detail: format!("bad linkReference start for {lib_name}"),
270                    })?;
271                let length = r["length"]
272                    .as_u64()
273                    .and_then(|v| usize::try_from(v).ok())
274                    .ok_or_else(|| Error::Artifact {
275                    detail: format!("bad linkReference length for {lib_name}"),
276                })?;
277                // Byte offsets → hex-char offsets (×2).
278                let begin = start.checked_mul(2);
279                let end = start.checked_add(length).and_then(|v| v.checked_mul(2));
280                let (begin, end) = begin.zip(end).ok_or_else(|| Error::Artifact {
281                    detail: format!("linkReference offset overflow for {lib_name}"),
282                })?;
283                hex_str.replace_range(begin..end, &addr_hex);
284            }
285        }
286    }
287
288    let bytes = hex::decode(&hex_str).map_err(|e| Error::Artifact {
289        detail: format!("invalid bytecode hex after linking {file}.{contract}: {e}"),
290    })?;
291    Ok(Bytes::from(bytes))
292}