|
| 1 | +// Copyright 2022-2024 Protocol Labs |
| 2 | +// SPDX-License-Identifier: Apache-2.0, MIT |
| 3 | + |
| 4 | +//! Deployer for Ethereum contracts and libraries. |
| 5 | +
|
| 6 | +pub mod utils; |
| 7 | + |
| 8 | +use std::collections::HashMap; |
| 9 | +use std::path::Path; |
| 10 | +use std::sync::Arc; |
| 11 | + |
| 12 | +use anyhow::{anyhow, Context, Result}; |
| 13 | +use ethers::abi::Tokenize; |
| 14 | +use ethers::contract::ContractFactory; |
| 15 | +use ethers::core::types as eth_types; |
| 16 | +use ethers::prelude::*; |
| 17 | +use fendermint_eth_hardhat::{ContractSourceAndName, DeploymentArtifact, Hardhat, FQN}; |
| 18 | +use fendermint_vm_actor_interface::diamond::EthContractMap; |
| 19 | +use fendermint_vm_actor_interface::ipc; |
| 20 | +use fendermint_vm_genesis::ipc::GatewayParams; |
| 21 | +use ipc_actors_abis::i_diamond::FacetCut; |
| 22 | +use ipc_provider::manager::evm::gas_estimator_middleware::Eip1559GasEstimatorMiddleware; |
| 23 | +use k256::ecdsa::SigningKey; |
| 24 | + |
| 25 | +use crate::utils::{collect_contracts, collect_facets, contract_src}; |
| 26 | + |
| 27 | +// 200 is used because some networks like the Calibration network and mainnet can be slow, |
| 28 | +// and the transaction deployment can fail even though the transaction is mined. |
| 29 | +const TRANSACTION_RECEIPT_RETRIES: usize = 200; |
| 30 | + |
| 31 | +type SignerWithFeeEstimator = |
| 32 | + Arc<Eip1559GasEstimatorMiddleware<SignerMiddleware<Provider<Http>, Wallet<SigningKey>>>>; |
| 33 | + |
| 34 | +pub struct DeployedContracts { |
| 35 | + pub registry: eth_types::Address, |
| 36 | + pub gateway: eth_types::Address, |
| 37 | +} |
| 38 | + |
| 39 | +#[repr(u8)] |
| 40 | +pub enum SubnetCreationPrivilege { |
| 41 | + Unrestricted = 0, |
| 42 | + Owner = 1, |
| 43 | +} |
| 44 | +/// Responsible for deploying Ethereum contracts and libraries. |
| 45 | +pub struct EthContractDeployer { |
| 46 | + hardhat: Hardhat, |
| 47 | + ipc_contracts: Vec<ContractSourceAndName>, |
| 48 | + top_contracts: EthContractMap, |
| 49 | + lib_addrs: HashMap<FQN, eth_types::Address>, |
| 50 | + provider: SignerWithFeeEstimator, |
| 51 | + chain_id: u64, |
| 52 | +} |
| 53 | + |
| 54 | +impl EthContractDeployer { |
| 55 | + /// Creates a new `EthContractDeployer` instance. |
| 56 | + pub fn new(hardhat: Hardhat, url: &str, private_key: &[u8], chain_id: u64) -> Result<Self> { |
| 57 | + let provider = Provider::<Http>::try_from(url).context("failed to create HTTP provider")?; |
| 58 | + let wallet: LocalWallet = |
| 59 | + LocalWallet::from_bytes(private_key).context("invalid private key")?; |
| 60 | + let wallet = wallet.with_chain_id(chain_id); |
| 61 | + let signer = SignerMiddleware::new(provider, wallet); |
| 62 | + let client = Eip1559GasEstimatorMiddleware::new(signer); |
| 63 | + |
| 64 | + let (ipc_contracts, top_contracts) = |
| 65 | + collect_contracts(&hardhat).context("failed to collect contracts")?; |
| 66 | + |
| 67 | + Ok(Self { |
| 68 | + hardhat, |
| 69 | + ipc_contracts, |
| 70 | + top_contracts, |
| 71 | + lib_addrs: HashMap::new(), |
| 72 | + provider: Arc::new(client), |
| 73 | + chain_id, |
| 74 | + }) |
| 75 | + } |
| 76 | + |
| 77 | + /// Deploys all contracts: |
| 78 | + /// first libraries, then the gateway and registry contracts. |
| 79 | + pub async fn deploy_all( |
| 80 | + &mut self, |
| 81 | + subnet_creation_privilege: SubnetCreationPrivilege, |
| 82 | + ) -> Result<DeployedContracts> { |
| 83 | + // Deploy all required libraries. |
| 84 | + for (lib_src, lib_name) in self.ipc_contracts.clone() { |
| 85 | + self.deploy_library(&lib_src, &lib_name) |
| 86 | + .await |
| 87 | + .with_context(|| format!("failed to deploy library {lib_name}"))?; |
| 88 | + } |
| 89 | + |
| 90 | + // Deploy the IPC Gateway contract. |
| 91 | + let gateway_addr = self.deploy_gateway().await?; |
| 92 | + |
| 93 | + // Deploy the IPC SubnetRegistry contract. |
| 94 | + let registry_addr = self |
| 95 | + .deploy_registry(gateway_addr, subnet_creation_privilege) |
| 96 | + .await?; |
| 97 | + |
| 98 | + Ok(DeployedContracts { |
| 99 | + registry: registry_addr, |
| 100 | + gateway: gateway_addr, |
| 101 | + }) |
| 102 | + } |
| 103 | + |
| 104 | + /// Deploys a library contract. |
| 105 | + /// |
| 106 | + /// Reads the library artifact, substitutes placeholders with correct addresses, |
| 107 | + /// deploys the library, and records its address. |
| 108 | + async fn deploy_library(&mut self, lib_src: &Path, lib_name: &str) -> Result<()> { |
| 109 | + let fqn = self.hardhat.fqn(lib_src, lib_name); |
| 110 | + tracing::info!("Deploying library: {}", lib_name); |
| 111 | + |
| 112 | + let artifact = self |
| 113 | + .hardhat |
| 114 | + .prepare_deployment_artifact(lib_src, lib_name, &self.lib_addrs) |
| 115 | + .with_context(|| format!("failed to load library bytecode for {fqn}"))?; |
| 116 | + |
| 117 | + let address = self.deploy_artifact(artifact, ()).await?; |
| 118 | + |
| 119 | + tracing::info!(?address, "Library deployed successfully"); |
| 120 | + self.lib_addrs.insert(fqn, address); |
| 121 | + Ok(()) |
| 122 | + } |
| 123 | + |
| 124 | + /// Deploys a top-level contract with the given constructor parameters. |
| 125 | + async fn deploy_contract<T>( |
| 126 | + &self, |
| 127 | + contract_name: &str, |
| 128 | + constructor_params: T, |
| 129 | + ) -> Result<eth_types::Address> |
| 130 | + where |
| 131 | + T: Tokenize, |
| 132 | + { |
| 133 | + let src = contract_src(contract_name); |
| 134 | + tracing::info!("Deploying top-level contract: {}", contract_name); |
| 135 | + |
| 136 | + let artifact = self |
| 137 | + .hardhat |
| 138 | + .prepare_deployment_artifact(&src, contract_name, &self.lib_addrs) |
| 139 | + .with_context(|| format!("failed to load {contract_name} bytecode"))?; |
| 140 | + |
| 141 | + let address = self.deploy_artifact(artifact, constructor_params).await?; |
| 142 | + tracing::info!(?address, "Contract deployed successfully"); |
| 143 | + |
| 144 | + Ok(address) |
| 145 | + } |
| 146 | + |
| 147 | + /// Deploys the provided deployment artifact with constructor parameters. |
| 148 | + async fn deploy_artifact<T>( |
| 149 | + &self, |
| 150 | + artifact: DeploymentArtifact, |
| 151 | + constructor_params: T, |
| 152 | + ) -> Result<eth_types::Address> |
| 153 | + where |
| 154 | + T: Tokenize, |
| 155 | + { |
| 156 | + let factory = ContractFactory::new( |
| 157 | + artifact.abi, |
| 158 | + artifact.bytecode.into(), |
| 159 | + self.provider.clone(), |
| 160 | + ); |
| 161 | + |
| 162 | + let deployer = factory |
| 163 | + .deploy(constructor_params) |
| 164 | + .context("failed to create deployer")?; |
| 165 | + |
| 166 | + // Send the transaction and wait for the receipt. |
| 167 | + let pending_tx = deployer |
| 168 | + .client() |
| 169 | + .send_transaction( |
| 170 | + deployer.tx.clone(), |
| 171 | + Some(BlockId::Number(BlockNumber::Pending)), |
| 172 | + ) |
| 173 | + .await?; |
| 174 | + |
| 175 | + tracing::info!(tx_hash = ?pending_tx.tx_hash(), "Transaction sent, awaiting confirmation"); |
| 176 | + |
| 177 | + let receipt = pending_tx |
| 178 | + .confirmations(1) |
| 179 | + .retries(TRANSACTION_RECEIPT_RETRIES) |
| 180 | + .await? |
| 181 | + .ok_or_else(|| anyhow!("failed to get transaction receipt"))?; |
| 182 | + |
| 183 | + let address = receipt |
| 184 | + .contract_address |
| 185 | + .ok_or_else(|| anyhow!("transaction receipt missing contract address"))?; |
| 186 | + |
| 187 | + Ok(address) |
| 188 | + } |
| 189 | + |
| 190 | + /// Deploys the gateway contract. |
| 191 | + async fn deploy_gateway(&self) -> Result<eth_types::Address> { |
| 192 | + use ipc::gateway::{ |
| 193 | + ConstructorParameters as GatewayConstructor, CONTRACT_NAME as GATEWAY_NAME, |
| 194 | + }; |
| 195 | + use ipc_api::subnet_id::SubnetID; |
| 196 | + |
| 197 | + let ipc_params = GatewayParams::new(SubnetID::new(self.chain_id, vec![])); |
| 198 | + let params = GatewayConstructor::new(ipc_params, vec![]) |
| 199 | + .context("failed to create gateway constructor parameters")?; |
| 200 | + |
| 201 | + let facets = self |
| 202 | + .collect_facets(GATEWAY_NAME) |
| 203 | + .context("failed to collect gateway facets")?; |
| 204 | + |
| 205 | + self.deploy_contract(GATEWAY_NAME, (facets, params)) |
| 206 | + .await |
| 207 | + .context("failed to deploy gateway contract") |
| 208 | + } |
| 209 | + |
| 210 | + /// Deploys the registry contract. |
| 211 | + async fn deploy_registry( |
| 212 | + &self, |
| 213 | + gateway_addr: eth_types::Address, |
| 214 | + subnet_creation_privilege: SubnetCreationPrivilege, |
| 215 | + ) -> Result<eth_types::Address> { |
| 216 | + use ipc::registry::{ |
| 217 | + ConstructorParameters as RegistryConstructor, CONTRACT_NAME as REGISTRY_NAME, |
| 218 | + }; |
| 219 | + |
| 220 | + let mut facets = self |
| 221 | + .collect_facets(REGISTRY_NAME) |
| 222 | + .context("failed to collect registry facets")?; |
| 223 | + |
| 224 | + // Ensure there are enough facets. |
| 225 | + if facets.len() < 9 { |
| 226 | + return Err(anyhow!( |
| 227 | + "expected at least 9 facets for registry contract, got {}", |
| 228 | + facets.len() |
| 229 | + )); |
| 230 | + } |
| 231 | + |
| 232 | + // Destructure the first 9 facets. |
| 233 | + let getter_facet = facets.remove(0); |
| 234 | + let manager_facet = facets.remove(0); |
| 235 | + let rewarder_facet = facets.remove(0); |
| 236 | + let checkpointer_facet = facets.remove(0); |
| 237 | + let pauser_facet = facets.remove(0); |
| 238 | + let diamond_loupe_facet = facets.remove(0); |
| 239 | + let diamond_cut_facet = facets.remove(0); |
| 240 | + let ownership_facet = facets.remove(0); |
| 241 | + let activity_facet = facets.remove(0); |
| 242 | + |
| 243 | + if facets.len() != 2 { |
| 244 | + return Err(anyhow!( |
| 245 | + "expected 2 extra facets for registry contract, got {}", |
| 246 | + facets.len() |
| 247 | + )); |
| 248 | + } |
| 249 | + |
| 250 | + let params = RegistryConstructor { |
| 251 | + gateway: gateway_addr, |
| 252 | + getter_facet: getter_facet.facet_address, |
| 253 | + manager_facet: manager_facet.facet_address, |
| 254 | + rewarder_facet: rewarder_facet.facet_address, |
| 255 | + pauser_facet: pauser_facet.facet_address, |
| 256 | + checkpointer_facet: checkpointer_facet.facet_address, |
| 257 | + diamond_cut_facet: diamond_cut_facet.facet_address, |
| 258 | + diamond_loupe_facet: diamond_loupe_facet.facet_address, |
| 259 | + ownership_facet: ownership_facet.facet_address, |
| 260 | + activity_facet: activity_facet.facet_address, |
| 261 | + subnet_getter_selectors: getter_facet.function_selectors, |
| 262 | + subnet_manager_selectors: manager_facet.function_selectors, |
| 263 | + subnet_rewarder_selectors: rewarder_facet.function_selectors, |
| 264 | + subnet_checkpointer_selectors: checkpointer_facet.function_selectors, |
| 265 | + subnet_pauser_selectors: pauser_facet.function_selectors, |
| 266 | + subnet_actor_diamond_cut_selectors: diamond_cut_facet.function_selectors, |
| 267 | + subnet_actor_diamond_loupe_selectors: diamond_loupe_facet.function_selectors, |
| 268 | + subnet_actor_ownership_selectors: ownership_facet.function_selectors, |
| 269 | + subnet_actor_activity_selectors: activity_facet.function_selectors, |
| 270 | + creation_privileges: subnet_creation_privilege as u8, |
| 271 | + }; |
| 272 | + |
| 273 | + self.deploy_contract(REGISTRY_NAME, (facets, params)) |
| 274 | + .await |
| 275 | + .context("failed to deploy registry contract") |
| 276 | + } |
| 277 | + |
| 278 | + /// Collects facet cuts for the diamond pattern for a specified top-level contract. |
| 279 | + fn collect_facets(&self, contract_name: &str) -> Result<Vec<FacetCut>> { |
| 280 | + collect_facets( |
| 281 | + contract_name, |
| 282 | + &self.hardhat, |
| 283 | + &self.top_contracts, |
| 284 | + &self.lib_addrs, |
| 285 | + ) |
| 286 | + } |
| 287 | +} |
0 commit comments