Skip to content

Commit 3b79c9d

Browse files
karlemcryptoAtwillLePremierHomme
authored
feat: add contracts deployer (#1318)
Co-authored-by: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Co-authored-by: LePremierHomme <57456510+LePremierHomme@users.noreply.github.com>
1 parent 5a5b2e9 commit 3b79c9d

12 files changed

Lines changed: 600 additions & 82 deletions

File tree

Cargo.lock

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fendermint/eth/deployer/Cargo.toml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[package]
2+
name = "fendermint_eth_deployer"
3+
description = "Utilities to deploy contracts on EVM chains"
4+
version = "0.1.0"
5+
authors.workspace = true
6+
edition.workspace = true
7+
license.workspace = true
8+
9+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
10+
11+
[dependencies]
12+
anyhow = { workspace = true }
13+
ethers-core = { workspace = true }
14+
hex = { workspace = true }
15+
fs-err = { workspace = true }
16+
serde = { workspace = true }
17+
serde_json = { workspace = true }
18+
ipc_actors_abis = { path = "../../../contract-bindings" }
19+
fendermint_vm_actor_interface = { path = "../../vm/actor_interface" }
20+
fendermint_eth_hardhat = { path = "../hardhat" }
21+
fendermint_vm_genesis = { path = "../../vm/genesis", features = ["arb"] }
22+
ethers = { workspace = true }
23+
tracing = { workspace = true }
24+
ipc-api = { path = "../../../ipc/api" }
25+
ipc-provider = { path = "../../../ipc/provider" }
26+
tokio = { workspace = true }
27+
tracing-subscriber = { workspace = true }

fendermint/eth/deployer/src/lib.rs

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
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

Comments
 (0)