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 std::collections::BTreeMap;
6
7use alloy::{
8    hex,
9    network::TransactionBuilder,
10    primitives::{
11        keccak256,
12        Address,
13        Bytes,
14        B256,
15    },
16    providers::Provider,
17    rpc::types::TransactionRequest,
18    sol_types::SolCall,
19};
20
21use crate::{
22    artifacts::Artifacts,
23    bindings::proxy::IUUPSUpgradeable,
24    error::{
25        Error,
26        Result,
27    },
28    factory::{
29        ensure_create2_deployer,
30        CREATE2_DEPLOYER,
31    },
32};
33
34/// Send a contract call with automatic retry on "nonce too low" errors.
35///
36/// Alloy's nonce manager can get stale when earlier calls fail at dry-run
37/// (e.g. a call reverting "already applied"). On each attempt the real nonce
38/// is fetched from the chain and set explicitly, bypassing the cached nonce
39/// manager entirely.
40///
41/// Usage:
42/// `send_with_nonce_retry!(contract.doSomething(args), "label", provider, sender)?;`
43#[macro_export]
44macro_rules! send_with_nonce_retry {
45    ($call_expr:expr, $label:expr, $provider:expr, $sender:expr) => {{
46        const MAX_RETRIES: u32 = 3;
47        let mut result: $crate::Result<alloy::rpc::types::TransactionReceipt> =
48            Err($crate::Error::Rpc {
49                detail: "unreachable".into(),
50            });
51        for attempt in 0..MAX_RETRIES {
52            let nonce =
53                alloy::providers::Provider::get_transaction_count($provider, $sender)
54                    .await
55                    .map_err(|e| $crate::Error::Rpc {
56                        detail: format!("{} failed to fetch nonce: {e}", $label),
57                    })?;
58            match ($call_expr).nonce(nonce).send().await {
59                Ok(pending) => {
60                    result =
61                        pending.get_receipt().await.map_err(|e| $crate::Error::Rpc {
62                            detail: format!("{} confirmation failed: {e}", $label),
63                        });
64                    break;
65                }
66                Err(e) => {
67                    let msg = e.to_string();
68                    let next_attempt = attempt.saturating_add(1);
69                    if msg.contains("nonce too low") && next_attempt < MAX_RETRIES {
70                        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
71                        continue;
72                    }
73                    result = Err($crate::Error::Rpc {
74                        detail: format!("{} send failed: {e}", $label),
75                    });
76                    break;
77                }
78            }
79        }
80        result
81    }};
82}
83
84/// Deploy a contract and return its address.
85pub async fn deploy_contract<P: Provider>(
86    provider: &P,
87    bytecode: Bytes,
88    label: &str,
89) -> Result<Address> {
90    deploy_contract_from(provider, bytecode, label, None).await
91}
92
93/// Deploy a contract, optionally fetching the sender's nonce explicitly.
94///
95/// Pass `sender` when mixing provider-managed and manually-nonce'd
96/// transactions in one run: the cached nonce manager goes stale otherwise.
97pub async fn deploy_contract_from<P: Provider>(
98    provider: &P,
99    bytecode: Bytes,
100    label: &str,
101    sender: Option<Address>,
102) -> Result<Address> {
103    let mut tx = TransactionRequest::default().with_deploy_code(bytecode);
104
105    if let Some(addr) = sender {
106        let nonce =
107            provider
108                .get_transaction_count(addr)
109                .await
110                .map_err(|e| Error::Rpc {
111                    detail: format!("failed to fetch nonce for {label}: {e}"),
112                })?;
113        tx = tx.with_nonce(nonce);
114    }
115
116    let pending = provider
117        .send_transaction(tx)
118        .await
119        .map_err(|e| Error::Rpc {
120            detail: format!("failed to send {label} deploy tx: {e}"),
121        })?;
122
123    let receipt = pending.get_receipt().await.map_err(|e| Error::Rpc {
124        detail: format!("failed to get {label} deploy receipt: {e}"),
125    })?;
126
127    receipt.contract_address.ok_or_else(|| Error::Rpc {
128        detail: format!("{label} deploy did not return contract address"),
129    })
130}
131
132/// Deploy `bytecode` with ABI-encoded constructor args appended.
133pub async fn deploy_with_ctor<P: Provider>(
134    provider: &P,
135    bytecode: &Bytes,
136    constructor_args: &[u8],
137    label: &str,
138    sender: Option<Address>,
139) -> Result<Address> {
140    let mut deploy_bytecode = bytecode.to_vec();
141    deploy_bytecode.extend_from_slice(constructor_args);
142    deploy_contract_from(provider, Bytes::from(deploy_bytecode), label, sender).await
143}
144
145/// Deploy an ERC1967 proxy pointing at `implementation` with `init_data` (the
146/// ABI-encoded initializer call).
147pub async fn deploy_proxy<P: Provider>(
148    provider: &P,
149    proxy_bytecode: &Bytes,
150    implementation: Address,
151    init_data: Bytes,
152    label: &str,
153    sender: Option<Address>,
154) -> Result<Address> {
155    // ERC1967Proxy constructor: (address implementation, bytes memory _data)
156    let constructor_args =
157        alloy::sol_types::SolValue::abi_encode_params(&(implementation, init_data));
158    deploy_with_ctor(provider, proxy_bytecode, &constructor_args, label, sender).await
159}
160
161/// Deploy an implementation from `artifacts` and put it behind a fresh
162/// ERC1967 proxy whose init data is `init_call` ABI-encoded. Returns the
163/// proxy address. The common shape of every UUPS deploy in the stack.
164pub async fn deploy_behind_proxy<P: Provider, C: SolCall>(
165    provider: &P,
166    artifacts: &Artifacts,
167    contract: &str,
168    init_call: &C,
169    sender: Option<Address>,
170) -> Result<Address> {
171    let implementation = deploy_contract_from(
172        provider,
173        artifacts.bytecode(contract)?,
174        &format!("{contract} (impl)"),
175        sender,
176    )
177    .await?;
178    let proxy_bytecode = artifacts.bytecode("ERC1967Proxy")?;
179    deploy_proxy(
180        provider,
181        &proxy_bytecode,
182        implementation,
183        init_call.abi_encode().into(),
184        &format!("{contract} (proxy)"),
185        sender,
186    )
187    .await
188}
189
190/// Upgrade a UUPS proxy: deploy `contract`'s current implementation from
191/// `artifacts`, then call `upgradeToAndCall(new_impl, data)` on the proxy.
192/// Returns the new implementation address. `data` is usually empty (state is
193/// already initialized); pass a re-initializer call when the upgrade needs
194/// one.
195pub async fn upgrade_uups<P: Provider>(
196    provider: &P,
197    artifacts: &Artifacts,
198    proxy: Address,
199    contract: &str,
200    data: Bytes,
201    sender: Option<Address>,
202) -> Result<Address> {
203    let new_impl = deploy_contract_from(
204        provider,
205        artifacts.bytecode(contract)?,
206        &format!("{contract} (new impl)"),
207        sender,
208    )
209    .await?;
210    let proxied = IUUPSUpgradeable::new(proxy, provider);
211    let call = proxied.upgradeToAndCall(new_impl, data);
212    let pending =
213        match sender {
214            Some(addr) => {
215                let nonce = provider.get_transaction_count(addr).await.map_err(|e| {
216                    Error::Rpc {
217                        detail: format!("{contract} upgrade failed to fetch nonce: {e}"),
218                    }
219                })?;
220                call.nonce(nonce).send().await
221            }
222            None => call.send().await,
223        }
224        .map_err(|e| Error::Rpc {
225            detail: format!("{contract} upgradeToAndCall send failed: {e}"),
226        })?;
227    pending.get_receipt().await.map_err(|e| Error::Rpc {
228        detail: format!("{contract} upgradeToAndCall confirmation failed: {e}"),
229    })?;
230    Ok(new_impl)
231}
232
233/// Deploy `salt ++ init_code` through the canonical CREATE2 deployer and
234/// check that code landed at `predicted`.
235///
236/// `sender` opts into explicit nonce management (see
237/// [`deploy_contract_from`]).
238pub(crate) async fn deploy_via_create2<P: Provider>(
239    provider: &P,
240    salt: B256,
241    init_code: &[u8],
242    predicted: Address,
243    label: &str,
244    sender: Option<Address>,
245) -> Result<()> {
246    let mut input = salt.to_vec();
247    input.extend_from_slice(init_code);
248    let mut tx = TransactionRequest::default()
249        .with_to(CREATE2_DEPLOYER)
250        .with_input(Bytes::from(input));
251    if let Some(addr) = sender {
252        let nonce =
253            provider
254                .get_transaction_count(addr)
255                .await
256                .map_err(|e| Error::Rpc {
257                    detail: format!("{label}: failed to fetch nonce: {e}"),
258                })?;
259        tx = tx.with_nonce(nonce);
260    }
261    let pending = provider
262        .send_transaction(tx)
263        .await
264        .map_err(|e| Error::Rpc {
265            detail: format!("{label}: CREATE2 deploy send failed: {e}"),
266        })?;
267    pending.get_receipt().await.map_err(|e| Error::Rpc {
268        detail: format!("{label}: CREATE2 deploy confirmation failed: {e}"),
269    })?;
270    if !code_present(provider, predicted, label).await? {
271        return Err(Error::Rpc {
272            detail: format!("{label}: no code at the predicted address {predicted}"),
273        });
274    }
275    Ok(())
276}
277
278async fn code_present<P: Provider>(
279    provider: &P,
280    address: Address,
281    label: &str,
282) -> Result<bool> {
283    let code = provider
284        .get_code_at(address)
285        .await
286        .map_err(|e| Error::Rpc {
287            detail: format!("{label}: failed to read code at {address}: {e}"),
288        })?;
289    Ok(!code.is_empty())
290}
291
292/// The salt every shared library deploys under. Fixed and empty on purpose:
293/// a CREATE2 address is a function of the deployer, the salt and the hash
294/// of the init code, and the init code is the whole key — the salt has
295/// nothing to add.
296pub const LIBRARY_SALT: B256 = B256::ZERO;
297
298/// Where a library with this creation code lands, on every chain: through
299/// the canonical [`CREATE2_DEPLOYER`] under [`LIBRARY_SALT`]. A pure
300/// function of the bytes, computable before anything is deployed.
301pub fn library_address(creation_code: &[u8]) -> Address {
302    CREATE2_DEPLOYER.create2(LIBRARY_SALT, keccak256(creation_code))
303}
304
305/// External libraries deployed once per distinct bytecode, and the address
306/// each linked contract substitutes for its placeholders.
307///
308/// Solidity links a contract against a library by `<File>.sol:<Name>`, so
309/// two files that each carry a copy of the same library — bb writes
310/// `RelationsLib` and `ZKTranscriptLib` into every verifier it generates —
311/// link by different keys. This groups those keys by the hash of the
312/// library's creation code, and that hash is the whole rule: identical
313/// bytecode is one deployment, and every contract whose artifact names
314/// that bytecode links against it; bytecode that differs is a deployment
315/// of its own. No version, release or list of which libraries happen to
316/// match takes part — a bb or solc bump that leaves a library identical
317/// keeps it shared, one that changes it separates it, and a contract can
318/// only ever link the bytes it was compiled against.
319///
320/// Each distinct library lands through the canonical CREATE2 deployer under
321/// [`LIBRARY_SALT`], so its address is [`library_address`] of its creation
322/// code: the same on every chain, and already holding code on a re-run,
323/// which is how a library that is deployed is found and reused with no
324/// record kept off chain. The deployer is installed if the chain lacks it,
325/// as [`ensure_factory`](crate::factory::ensure_factory) does; there is no
326/// plain-CREATE fallback, because an address that is not a function of the
327/// code is one a later run cannot find.
328///
329/// Creation code rather than runtime code, because it is what the chain
330/// receives and what the address derives from, and equal creation code is
331/// equal runtime code.
332#[derive(Clone, Debug, Default, PartialEq, Eq)]
333pub struct Libraries {
334    /// `(file, library)` as `linkReferences` names it -> the address linked.
335    linked: BTreeMap<(String, String), Address>,
336    /// Every distinct creation code, by hash -> its address, and whether
337    /// this call deployed it (`true`) or found its code in place (`false`).
338    distinct: BTreeMap<B256, (Address, bool)>,
339}
340
341impl Libraries {
342    /// Deploy every library the `(file, contract)` artifacts link, once per
343    /// distinct bytecode, skipping any whose code is already at its address.
344    /// A library that links libraries of its own has those resolved first.
345    /// Contracts that link nothing contribute nothing and cost nothing.
346    ///
347    /// `sender` opts into explicit nonce management (see
348    /// [`deploy_contract_from`]).
349    pub async fn deploy<P: Provider>(
350        provider: &P,
351        artifacts: &Artifacts,
352        contracts: &[(&str, &str)],
353        sender: Option<Address>,
354    ) -> Result<Self> {
355        let mut libraries = Self::default();
356        for (file, contract) in contracts {
357            libraries
358                .resolve(provider, artifacts, file, contract, sender)
359                .await?;
360        }
361        Ok(libraries)
362    }
363
364    /// The address `<file>.sol:<library>` links against, if it is in the
365    /// set.
366    pub fn address(&self, file: &str, library: &str) -> Option<Address> {
367        self.linked
368            .get(&(file.to_owned(), library.to_owned()))
369            .copied()
370    }
371
372    /// Every distinct library, as the hash of its creation code and its
373    /// address. One entry per deployment, however many files name it.
374    pub fn distinct(&self) -> impl Iterator<Item = (B256, Address)> + '_ {
375        self.distinct
376            .iter()
377            .map(|(hash, (address, _))| (*hash, *address))
378    }
379
380    /// The addresses this call sent a deploy for, as opposed to found.
381    pub fn deployed(&self) -> impl Iterator<Item = Address> + '_ {
382        self.distinct
383            .values()
384            .filter(|(_, deployed)| *deployed)
385            .map(|(address, _)| *address)
386    }
387
388    /// The creation bytecode of `<file>.sol:<contract>` with every library
389    /// it links substituted from the set. Pure: no transaction. Errors when
390    /// the artifact names a library the set does not hold.
391    pub fn link(
392        &self,
393        artifacts: &Artifacts,
394        file: &str,
395        contract: &str,
396    ) -> Result<Bytes> {
397        let mut hex_str = artifacts.bytecode_hex(file, contract)?;
398        for (lib_path, libs) in artifacts.link_references(file, contract)? {
399            let lib_file = file_stem(&lib_path)?;
400            for (lib_name, refs) in libs.as_object().into_iter().flatten() {
401                let address = self.address(lib_file, lib_name).ok_or_else(|| {
402                    Error::Artifact {
403                        detail: format!(
404                            "{file}.sol:{contract} links {lib_file}.sol:{lib_name}, which \
405                             is not among the deployed libraries"
406                        ),
407                    }
408                })?;
409                substitute(&mut hex_str, refs, address, lib_name)?;
410            }
411        }
412        if hex_str.contains("__$") {
413            return Err(Error::Artifact {
414                detail: format!(
415                    "{file}.sol:{contract} still has a link placeholder after linking"
416                ),
417            });
418        }
419        let bytes = hex::decode(&hex_str).map_err(|e| Error::Artifact {
420            detail: format!(
421                "invalid bytecode hex after linking {file}.sol:{contract}: {e}"
422            ),
423        })?;
424        Ok(Bytes::from(bytes))
425    }
426
427    /// Put every library `<file>.sol:<contract>` links into the set.
428    async fn resolve<P: Provider>(
429        &mut self,
430        provider: &P,
431        artifacts: &Artifacts,
432        file: &str,
433        contract: &str,
434        sender: Option<Address>,
435    ) -> Result<()> {
436        for (lib_path, libs) in artifacts.link_references(file, contract)? {
437            let lib_file = file_stem(&lib_path)?;
438            for lib_name in libs.as_object().into_iter().flatten().map(|(name, _)| name) {
439                let key = (lib_file.to_owned(), lib_name.clone());
440                if self.linked.contains_key(&key) {
441                    continue;
442                }
443                // The library's own libraries first, so that its creation
444                // code — and so its hash and its address — is final.
445                Box::pin(self.resolve(provider, artifacts, lib_file, lib_name, sender))
446                    .await?;
447                let code = self.link(artifacts, lib_file, lib_name)?;
448                let hash = keccak256(&code);
449                let address = match self.distinct.get(&hash) {
450                    Some((address, _)) => *address,
451                    None => {
452                        let label = format!("{lib_file}.sol:{lib_name} (library)");
453                        let address = library_address(&code);
454                        let found = code_present(provider, address, &label).await?;
455                        if !found {
456                            ensure_create2_deployer(provider).await?;
457                            deploy_via_create2(
458                                provider,
459                                LIBRARY_SALT,
460                                &code,
461                                address,
462                                &label,
463                                sender,
464                            )
465                            .await?;
466                        }
467                        self.distinct.insert(hash, (address, !found));
468                        address
469                    }
470                };
471                self.linked.insert(key, address);
472            }
473        }
474        Ok(())
475    }
476}
477
478/// `contracts/circuits/X.sol` -> `X`, the artifact directory's name.
479fn file_stem(path: &str) -> Result<&str> {
480    std::path::Path::new(path)
481        .file_stem()
482        .and_then(|s| s.to_str())
483        .ok_or_else(|| Error::Artifact {
484            detail: format!("bad library file path {path}"),
485        })
486}
487
488/// Write `address` over every `{start, length}` placeholder in `refs`.
489fn substitute(
490    hex_str: &mut String,
491    refs: &serde_json::Value,
492    address: Address,
493    library: &str,
494) -> Result<()> {
495    let addr_hex = hex::encode(address.as_slice()); // 40 hex chars
496    for r in refs.as_array().into_iter().flatten() {
497        let start = r["start"]
498            .as_u64()
499            .and_then(|v| usize::try_from(v).ok())
500            .ok_or_else(|| Error::Artifact {
501                detail: format!("bad linkReference start for {library}"),
502            })?;
503        let length = r["length"]
504            .as_u64()
505            .and_then(|v| usize::try_from(v).ok())
506            .ok_or_else(|| Error::Artifact {
507                detail: format!("bad linkReference length for {library}"),
508            })?;
509        if length != Address::len_bytes() {
510            return Err(Error::Artifact {
511                detail: format!(
512                    "linkReference for {library} is {length} bytes, not an address"
513                ),
514            });
515        }
516        // Byte offsets → hex-char offsets (×2).
517        let begin = start.checked_mul(2);
518        let end = start.checked_add(length).and_then(|v| v.checked_mul(2));
519        let (begin, end) = begin.zip(end).ok_or_else(|| Error::Artifact {
520            detail: format!("linkReference offset overflow for {library}"),
521        })?;
522        if end > hex_str.len() {
523            return Err(Error::Artifact {
524                detail: format!("linkReference for {library} runs past the bytecode"),
525            });
526        }
527        hex_str.replace_range(begin..end, &addr_hex);
528    }
529    Ok(())
530}
531
532/// The creation bytecode of `<file>.sol:<contract>` with every library it
533/// links deployed and substituted: [`Libraries::deploy`] over the one
534/// contract, then [`Libraries::link`]. Mirrors what `forge` does
535/// automatically, except that a library already at its address is reused
536/// rather than deployed again. For artifacts with no `linkReferences` this
537/// behaves like [`Artifacts::bytecode_named`] and sends nothing.
538///
539/// The bb-generated UltraHonk verifiers are what goes through here: each
540/// links `RelationsLib` and `ZKTranscriptLib`, and
541/// [`deploy_honk_verifier`](crate::circuits::deploy_honk_verifier) is the
542/// call that links and deploys one.
543pub async fn load_linked_bytecode<P: Provider>(
544    provider: &P,
545    artifacts: &Artifacts,
546    file: &str,
547    contract: &str,
548    sender: Option<Address>,
549) -> Result<Bytes> {
550    Libraries::deploy(provider, artifacts, &[(file, contract)], sender)
551        .await?
552        .link(artifacts, file, contract)
553}