Skip to main content

paralock_evm/
deploy.rs

1//! Local deployment helper, the Rust counterpart of `@paralock/evm`'s `deployParalockContracts`.
2
3use std::collections::BTreeMap;
4use std::time::Duration;
5
6use alloy::primitives::{Address, B256, U256, keccak256};
7
8use crate::artifacts::{ContractArtifact, ParalockContract};
9use crate::bindings::{ITIP20Factory, ITIP20Issuer, MockUSD};
10use crate::config::{EvmConfig, EvmSigner};
11use crate::connection::{EvmConnection, TxRequest};
12use crate::error::{EvmError, RevertSource};
13use crate::networks::{ParalockDeployment, known_network};
14use crate::tempo::{TEMPO_TOKENS, TIP20_FACTORY, is_tempo_chain_id};
15
16/// 1,000,000,000 USD (6 decimals): what `Deploy.s.sol` mints per account.
17pub const LOCAL_MINT_AMOUNT: U256 = U256::from_limbs([1_000_000_000_000_000, 0, 0, 0]);
18
19/// Salt of the local OUSD stand-in, as in `contracts/script/Deploy.s.sol`: `keccak256("paralock/ousd")`.
20pub fn local_ousd_salt() -> B256 {
21    keccak256("paralock/ousd")
22}
23
24/// An earlier deployment to reuse where it is still current.
25#[derive(Clone, Debug, Default, PartialEq, Eq)]
26pub struct PreviousDeployment {
27    /// `ParalockHTLC`, reused if the chain still holds exactly this build's runtime code.
28    pub htlc: Option<Address>,
29    /// `ParalockAdaptorEscrow`, reused under the same rule.
30    pub adaptor_escrow: Option<Address>,
31    /// Kept when a contract is reused, so event scans still cover it.
32    pub start_block: Option<u64>,
33    /// Stablecoins by symbol; a `USDC` MockUSD is reused if it exists and the deployer is its minter.
34    pub tokens: BTreeMap<String, Address>,
35}
36
37impl From<&ParalockDeployment> for PreviousDeployment {
38    fn from(d: &ParalockDeployment) -> Self {
39        Self {
40            htlc: Some(d.htlc),
41            adaptor_escrow: Some(d.adaptor_escrow),
42            start_block: Some(d.start_block),
43            tokens: d.tokens.clone(),
44        }
45    }
46}
47
48/// Options of [`deploy_paralock_contracts`].
49#[derive(Clone, Debug)]
50#[non_exhaustive]
51pub struct DeployOptions {
52    /// RPC endpoint of the local chain.
53    pub rpc_url: String,
54    /// Deployer (on the Paralock devnet, anvil dev account 0).
55    pub signer: EvmSigner,
56    /// Expected chain id. Default: asked from the node.
57    pub chain_id: Option<u64>,
58    /// Chain family recorded in the deployment (`tempo`, `base`, …). Default: `tempo` on Tempo chains, else none.
59    pub chain: Option<String>,
60    /// Treat the chain as Tempo (default: by chain id, see [`crate::TEMPO_CHAIN_IDS`]).
61    pub tempo: Option<bool>,
62    /// Tempo fee token for the deployment transactions (see [`EvmConfig::fee_token`]).
63    pub fee_token: Option<Address>,
64    /// An earlier deployment to reuse.
65    pub previous: PreviousDeployment,
66    /// Accounts that receive `mint_amount` of a newly created stablecoin. Default: the deployer.
67    pub mint_to: Option<Vec<Address>>,
68    /// Base units minted to each of `mint_to`. Default [`LOCAL_MINT_AMOUNT`].
69    pub mint_amount: U256,
70    /// Receipt timeout per transaction. Default 180 s.
71    pub receipt_timeout: Duration,
72}
73
74impl DeployOptions {
75    /// Deploy to `rpc_url` with `signer`, everything else default.
76    pub fn new(rpc_url: impl Into<String>, signer: impl Into<EvmSigner>) -> Self {
77        Self {
78            rpc_url: rpc_url.into(),
79            signer: signer.into(),
80            chain_id: None,
81            chain: None,
82            tempo: None,
83            fee_token: None,
84            previous: PreviousDeployment::default(),
85            mint_to: None,
86            mint_amount: LOCAL_MINT_AMOUNT,
87            receipt_timeout: Duration::from_secs(180),
88        }
89    }
90}
91
92/// What [`deploy_paralock_contracts`] kept instead of creating.
93#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
94pub struct Reused {
95    /// `ParalockHTLC` was reused.
96    pub htlc: bool,
97    /// `ParalockAdaptorEscrow` was reused.
98    pub adaptor_escrow: bool,
99    /// The quote stablecoin was reused.
100    pub quote_token: bool,
101}
102
103/// Result of [`deploy_paralock_contracts`].
104#[derive(Clone, Debug, PartialEq, Eq)]
105pub struct DeployResult {
106    /// The deployment (the shape of `contracts/deployments/<chainId>.json`).
107    pub deployment: ParalockDeployment,
108    /// What already existed and was kept.
109    pub reused: Reused,
110}
111
112/// Deploys `ParalockHTLC`, `ParalockAdaptorEscrow` and a local stablecoin from the embedded artifacts, the way
113/// `contracts/script/Deploy.s.sol` does on the Paralock devnet, without Foundry:
114///
115/// - on a Tempo chain (`anvil --network tempo`): an "OUSD" stand-in created through the TIP-20 factory precompile at
116///   an address fixed by (deployer, salt), so it is reused once it exists; minted to `mint_to`;
117/// - elsewhere: a `MockUSD` "USDC" (6 decimals, only the deployer can mint), minted to `mint_to`.
118///
119/// Contracts in `previous` are reused when still current. **Local chains only**: it refuses every network in
120/// [`crate::KNOWN_NETWORKS`] ([`EvmError::PublicNetwork`]); deploy there with `contracts/script/Deploy.s.sol`.
121pub async fn deploy_paralock_contracts(options: DeployOptions) -> Result<DeployResult, EvmError> {
122    let chain_id = match options.chain_id {
123        Some(id) => id,
124        None => crate::connection::fetch_chain_id(&options.rpc_url).await?,
125    };
126    if let Some(known) = known_network(chain_id) {
127        return Err(EvmError::PublicNetwork { chain_id, name: known.name });
128    }
129    let tempo = options.tempo.unwrap_or(options.fee_token.is_some() || is_tempo_chain_id(chain_id));
130    let mut config = EvmConfig::new(options.rpc_url.clone(), chain_id)
131        .with_signer(options.signer.clone())
132        .with_tempo(tempo)
133        .with_receipt_timeout(options.receipt_timeout);
134    config.fee_token = options.fee_token;
135    let conn = EvmConnection::connect(config).await?;
136    let deployer = options.signer.address();
137    let previous = &options.previous;
138    let mint_to = options.mint_to.clone().unwrap_or_else(|| vec![deployer]);
139
140    let htlc_artifact = ParalockContract::Htlc.artifact()?;
141    let escrow_artifact = ParalockContract::AdaptorEscrow.artifact()?;
142    let mut reused = Reused {
143        htlc: is_current(&conn, previous.htlc, &htlc_artifact).await?,
144        adaptor_escrow: is_current(&conn, previous.adaptor_escrow, &escrow_artifact).await?,
145        quote_token: false,
146    };
147    // Reused contracts keep the earlier start block, so event scans still cover them.
148    let start_block = match previous.start_block {
149        Some(start) if reused.htlc || reused.adaptor_escrow => start,
150        _ => conn.block_number().await?,
151    };
152
153    let htlc = match previous.htlc {
154        Some(address) if reused.htlc => {
155            tracing::info!(%address, "reusing ParalockHTLC");
156            address
157        }
158        _ => deploy(&conn, &htlc_artifact, true).await?,
159    };
160    let adaptor_escrow = match previous.adaptor_escrow {
161        Some(address) if reused.adaptor_escrow => {
162            tracing::info!(%address, "reusing ParalockAdaptorEscrow");
163            address
164        }
165        _ => deploy(&conn, &escrow_artifact, true).await?,
166    };
167
168    let mut tokens = BTreeMap::new();
169    let quote_token = if tempo {
170        let salt = local_ousd_salt();
171        let token: Address = conn
172            .read(
173                TIP20_FACTORY,
174                &ITIP20Factory::getTokenAddressCall { sender: deployer, salt },
175                None,
176                Some(RevertSource::Token),
177            )
178            .await?;
179        reused.quote_token =
180            conn.read(TIP20_FACTORY, &ITIP20Factory::isTIP20Call { token }, None, Some(RevertSource::Token)).await?;
181        if reused.quote_token {
182            tracing::info!(%token, "reusing the local OUSD stand-in");
183        } else {
184            let create = ITIP20Factory::createTokenCall {
185                name: "Open USD (Paralock local)".to_owned(),
186                symbol: "OUSD".to_owned(),
187                currency: "USD".to_owned(),
188                quoteToken: TEMPO_TOKENS.path_usd,
189                admin: deployer,
190                salt,
191            };
192            conn.send_and_confirm(TxRequest::call(TIP20_FACTORY, &create, Some(RevertSource::Token))).await?;
193            let issuer: B256 =
194                conn.read(token, &ITIP20Issuer::ISSUER_ROLECall {}, None, Some(RevertSource::Token)).await?;
195            let grant = ITIP20Issuer::grantRoleCall { role: issuer, account: deployer };
196            conn.send_and_confirm(TxRequest::call(token, &grant, Some(RevertSource::Token))).await?;
197            for to in &mint_to {
198                let mint = ITIP20Issuer::mintCall { to: *to, amount: options.mint_amount };
199                conn.send_and_confirm(TxRequest::call(token, &mint, Some(RevertSource::Token))).await?;
200            }
201            tracing::info!(%token, "created the local OUSD stand-in");
202        }
203        tokens.insert("AlphaUSD".to_owned(), TEMPO_TOKENS.alpha_usd);
204        tokens.insert("BetaUSD".to_owned(), TEMPO_TOKENS.beta_usd);
205        tokens.insert("OUSD".to_owned(), token);
206        tokens.insert("PathUSD".to_owned(), TEMPO_TOKENS.path_usd);
207        tokens.insert("ThetaUSD".to_owned(), TEMPO_TOKENS.theta_usd);
208        "OUSD"
209    } else {
210        let previous_usdc = previous.tokens.get("USDC").copied();
211        reused.quote_token = is_own_mock_usd(&conn, previous_usdc, deployer).await?;
212        let token = match previous_usdc {
213            Some(token) if reused.quote_token => {
214                tracing::info!(%token, "reusing MockUSD");
215                token
216            }
217            _ => {
218                let token = deploy(&conn, &ParalockContract::MockUsd.artifact()?, false).await?;
219                for to in &mint_to {
220                    let mint = MockUSD::mintCall { to: *to, amount: options.mint_amount };
221                    conn.send_and_confirm(TxRequest::call(token, &mint, Some(RevertSource::Token))).await?;
222                }
223                token
224            }
225        };
226        tokens.insert("USDC".to_owned(), token);
227        "USDC"
228    };
229
230    let chain = options.chain.clone().or_else(|| tempo.then(|| "tempo".to_owned()));
231    let deployment = ParalockDeployment {
232        adaptor_escrow,
233        chain,
234        chain_id,
235        deployer,
236        htlc,
237        quote_token: quote_token.to_owned(),
238        start_block,
239        tokens,
240    };
241    Ok(DeployResult { deployment, reused })
242}
243
244/// Deploys `artifact`; with `check_code`, verifies that the chain holds exactly its runtime code afterwards.
245async fn deploy(conn: &EvmConnection, artifact: &ContractArtifact, check_code: bool) -> Result<Address, EvmError> {
246    let result = conn.send_and_confirm(TxRequest::deploy(artifact.bytecode.clone())).await?;
247    let address = result.contract_address.ok_or_else(|| {
248        EvmError::InvalidResponse(format!("{} deployment {} created no contract", artifact.contract_name, result.hash))
249    })?;
250    if check_code && !conn.holds_code(address, &artifact.deployed_bytecode).await? {
251        return Err(EvmError::InvalidResponse(format!(
252            "{} at {address} does not hold the expected runtime code",
253            artifact.contract_name
254        )));
255    }
256    tracing::info!(contract = %artifact.contract_name, %address, "deployed");
257    Ok(address)
258}
259
260/// True if `address` holds exactly `artifact`'s runtime code (the rule of [`EvmConnection::verify_escrow_code`]).
261async fn is_current(
262    conn: &EvmConnection,
263    address: Option<Address>,
264    artifact: &ContractArtifact,
265) -> Result<bool, EvmError> {
266    match address {
267        Some(address) => conn.holds_code(address, &artifact.deployed_bytecode).await,
268        None => Ok(false),
269    }
270}
271
272/// True if `address` is a MockUSD that `deployer` can mint.
273async fn is_own_mock_usd(conn: &EvmConnection, address: Option<Address>, deployer: Address) -> Result<bool, EvmError> {
274    let Some(address) = address else { return Ok(false) };
275    if conn.code_at(address).await?.is_empty() {
276        return Ok(false);
277    }
278    match conn.read(address, &MockUSD::minterCall {}, None, Some(RevertSource::Token)).await {
279        Ok(minter) => Ok(minter == deployer),
280        Err(EvmError::TransactionReverted { .. } | EvmError::InvalidResponse(_)) => Ok(false),
281        Err(err) => Err(err),
282    }
283}