diff --git a/Makefile b/Makefile index 97d3474..01af978 100644 --- a/Makefile +++ b/Makefile @@ -138,6 +138,7 @@ build-contracts: make braavos-account-cairo make argent-contracts-starknet + artifacts-linux: make setup-linux make build-contracts diff --git a/Readme.md b/Readme.md index b715119..fdf5d1f 100644 --- a/Readme.md +++ b/Readme.md @@ -38,6 +38,11 @@ There are three test in the repository : - You need to comment/remove the #[ignore] tags in [src/tests/mod.rs](src/tests/mod.rs) file - Only one test can be run at one time as all the tests are e2e tests. - You also would need to restart both the chains after running each test. +- You would need to clone [Madara](https://github.com/madara-alliance/madara.git) repo by running : + + ```shell + git clone --branch d188aa91efa78bcc54f92aa1035295fd50e068d2 https://github.com/madara-alliance/madara.git + ``` ```shell # 1. Run madara instance with eth as settlement layer : @@ -100,25 +105,19 @@ RUST_LOG=info cargo run -- --dev ### Contract Descriptions 🗒️ -| Contract | Source Link | Local Path | -| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | -| ERC20 (starkgate) | | [src/contracts/erc20.sierra.json](./src/contracts/erc20.sierra.json) | -| ERC20 (legacy : starknet) | | [src/contracts/erc20.json](./src/contracts/erc20.json) | -| OpenZeppelinAccount (legacy : starknet) | | [src/contracts/OpenZeppelinAccount.json](./src/contracts/OpenZeppelinAccount.json) | -| OpenZeppelinAccount (modified : openzeppelin) | [src/contracts/OpenZeppelinAccountCairoOne.sierra.json](src/contracts/OpenZeppelinAccountCairoOne.sierra.json) | [src/contracts/OpenZeppelinAccountCairoOne.sierra.json](./src/contracts/OpenZeppelinAccountCairoOne.sierra.json) | -| Proxy (legacy : starknet) | | [src/contracts/proxy_legacy.json](./src/contracts/proxy_legacy.json) | -| ETH token bridge (legacy : starkgate) | | [src/contracts/legacy_token_bridge.json](./src/contracts/legacy_token_bridge.json) | -| UDC (Universal Deployer Contract) | | [src/contracts/udc.json](./src/contracts/udc.json) | +| Contract | Source Link | Local Path | +| --------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| OpenZeppelinAccount (legacy : starknet) | | [src/contracts/OpenZeppelinAccount.json](./src/contracts/OpenZeppelinAccount.json) | +| OpenZeppelinAccount (modified : openzeppelin) | [src/contracts/account.cairo](src/contracts/account.cairo) | [src/contracts/OpenZeppelinAccountCairoOne.sierra.json](./src/contracts/OpenZeppelinAccountCairoOne.sierra.json) | +| UDC (Universal Deployer Contract) | | [src/contracts/udc.json](./src/contracts/udc.json) | Here are some contract descriptions on why they are used in our context. -- `ERC20 (starkgate)` : This ERC20 contracts works without a proxy and is used by erc20 token bridge in - order to deploy the token on L2. -- `ERC20 (legacy : starknet)` : This contract is used for deploying the implementation of ETH token on L2. -- `ERC20 token bridge (starkgate)` : Contract for Token bridge. - `OpenZeppelinAccount (legacy : starknet)` : Contract used for declaring a temp account for declaring V1 contract that will be used to deploy the user account with provided private key in env. +- `OpenZeppelinAccount (modified : openzeppelin)` : OZ account contract modified to include `deploy_contract` + function as we deploy the UDC towards the end of the bootstrapper setup. > [!IMPORTANT] > For testing in Github CI we are using the madara binary build with diff --git a/build-artifacts/Dockerfile b/build-artifacts/Dockerfile index d908e0b..5f02ce2 100644 --- a/build-artifacts/Dockerfile +++ b/build-artifacts/Dockerfile @@ -31,11 +31,6 @@ WORKDIR /app/ RUN rm -rf build RUN ./build.sh -# Create build directory and demo file -RUN mkdir -p /app/build/Release -RUN touch /app/build/Release/demo.txt -RUN echo 'Hello, World2!' > /app/build/Release/demo.txt - # Create a simpler entrypoint script RUN echo '#!/bin/sh' > /entrypoint.sh && \ echo 'cp -r /app/build/Release/src/* /mnt/' >> /entrypoint.sh && \ diff --git a/src/main.rs b/src/main.rs index 8656f60..000b345 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,7 @@ use std::str::FromStr; use clap::{Parser, ValueEnum}; use contract_clients::utils::RpcAccount; use dotenv::dotenv; -use ethers::abi::Address; +use ethers::abi::{AbiEncode, Address}; use inline_colorization::*; use serde::{Deserialize, Serialize}; use setup_scripts::argent::ArgentSetupOutput; @@ -65,13 +65,15 @@ pub struct CliArgs { output_file: Option, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub enum CoreContractMode { Production, Dev, } -#[derive(Serialize, Deserialize)] +// TODO : There is a lot of optional stuff in the config which is needed if we run +// TODO : (continued.) individual commands. We need to think of a better design. +#[derive(Serialize, Deserialize, Clone)] pub struct ConfigFile { pub eth_rpc: String, pub eth_priv_key: String, @@ -146,7 +148,7 @@ pub async fn main() { println!("{color_red}{}{color_reset}", BANNER); // Load config from file or use defaults - let config_file = match args.config { + let mut config_file = match args.config { Some(path) => { let file = File::open(path).expect("Failed to open config file"); serde_json::from_reader(file).expect("Failed to parse config file") @@ -174,7 +176,7 @@ pub async fn main() { ..Default::default() } } - BootstrapMode::SetupL2 => setup_l2(&config_file, &clients).await, + BootstrapMode::SetupL2 => setup_l2(&mut config_file, &clients).await, BootstrapMode::EthBridge => { let core_contract_client = get_core_contract_client(&config_file, &clients); let output = setup_eth_bridge(account, &core_contract_client, &config_file, &clients).await; @@ -199,7 +201,7 @@ pub async fn main() { &config_file, &clients, Felt::from_str( - &config_file.udc_address.clone().expect("UDC Address not available in config. Run with SetupL2"), + &config_file.udc_address.clone().expect("UDC Address not available in config. Run with mode UDC"), ) .expect("Unable to get UDC address"), ) @@ -266,7 +268,7 @@ pub struct BootstrapperOutput { pub braavos_setup_outputs: Option, } -pub async fn bootstrap(config_file: &ConfigFile, clients: &Clients) -> BootstrapperOutput { +pub async fn bootstrap(config_file: &mut ConfigFile, clients: &Clients) -> BootstrapperOutput { // setup core contract (L1) let core_contract_client = setup_core_contract(config_file, clients).await; @@ -437,11 +439,14 @@ async fn setup_braavos<'a>( braavos_setup_outputs } -pub async fn setup_l2(config_file: &ConfigFile, clients: &Clients) -> BootstrapperOutput { - let account = get_account(clients, config_file).await; +pub async fn setup_l2(config_file: &mut ConfigFile, clients: &Clients) -> BootstrapperOutput { + // Had to create a temporary clone otherwise the `ConfigFile` + // will be dropped after passing into `get_account` function. + let config_file_clone = &config_file.clone(); + let account = get_account(clients, config_file_clone).await; let core_contract_client = get_core_contract_client(config_file, clients); - println!(">>> get core contract client done"); + // setup eth bridge let eth_bridge_setup_outputs = setup_eth_bridge(Some(account.clone()), &core_contract_client, config_file, clients).await; @@ -460,28 +465,15 @@ pub async fn setup_l2(config_file: &ConfigFile, clients: &Clients) -> Bootstrapp let braavos_setup_outputs = setup_braavos(Some(account.clone()), config_file, clients, udc_setup_outputs.udc_address).await; - // upgrading the bridge : - let account = build_single_owner_account( - clients.provider_l2(), - &config_file.rollup_priv_key, - &account.address().to_hex_string(), - false, - ) - .await; - upgrade_eth_token_to_cairo_1( - &account, - clients.provider_l2(), - eth_bridge_setup_outputs.clone().l2_eth_proxy_address, - ) - .await; - upgrade_eth_bridge_to_cairo_1( - &account, - clients.provider_l2(), - eth_bridge_setup_outputs.clone().l2_eth_bridge_proxy_address, - eth_bridge_setup_outputs.clone().l2_eth_proxy_address, - ) - .await; - upgrade_l1_bridge(eth_bridge_setup_outputs.clone().l1_bridge_address, config_file).await.unwrap(); + // upgrading the eth bridge + config_file.l1_eth_bridge_address = Some(format!( + "0x{}", + eth_bridge_setup_outputs.l1_bridge_address.encode_hex().trim_start_matches("0x").trim_start_matches('0') + )); + config_file.l2_eth_token_proxy_address = Some(eth_bridge_setup_outputs.l2_eth_proxy_address.to_hex_string()); + config_file.l2_eth_bridge_proxy_address = + Some(eth_bridge_setup_outputs.l2_eth_bridge_proxy_address.to_hex_string()); + upgrade_eth_bridge(Some(account), config_file, clients).await.expect("Unable to upgrade ETH bridge."); BootstrapperOutput { eth_bridge_setup_outputs: Some(eth_bridge_setup_outputs), diff --git a/src/setup_scripts/upgrade_eth_token.rs b/src/setup_scripts/upgrade_eth_token.rs index 8b82102..8e7c354 100644 --- a/src/setup_scripts/upgrade_eth_token.rs +++ b/src/setup_scripts/upgrade_eth_token.rs @@ -13,6 +13,19 @@ use crate::utils::constants::{ }; use crate::utils::wait_for_transaction; +/// Upgrades the Ethereum token contract implementation to Cairo 1 through a series of steps: +/// 1. Declares and deploys an ETH EIC (External Implementation Contract) +/// 2. Declares and deploys a new ETH token implementation +/// 3. Performs the upgrade process by: +/// - Adding the new implementation to the proxy +/// - Upgrading to the new implementation +/// - Registering governance and upgrade administrators +/// - Adding and replacing the new implementation class hash +/// +/// # Arguments +/// * `account` - The RPC account used to perform the transactions +/// * `rpc_provider_l2` - JSON-RPC client for L2 network communication +/// * `l2_eth_token_address` - The address of the existing ETH token contract on L2 pub async fn upgrade_eth_token_to_cairo_1( account: &RpcAccount<'_>, rpc_provider_l2: &JsonRpcClient, diff --git a/src/setup_scripts/upgrade_l1_bridge.rs b/src/setup_scripts/upgrade_l1_bridge.rs index ea33478..b6fca60 100644 --- a/src/setup_scripts/upgrade_l1_bridge.rs +++ b/src/setup_scripts/upgrade_l1_bridge.rs @@ -20,6 +20,25 @@ abigen!( abigen!(EthereumNewBridge, "artifacts/upgrade-contracts/eth_bridge_upgraded.json"); abigen!(EthereumNewBridgeEIC, "artifacts/upgrade-contracts/eic_eth_bridge.json"); +/// Upgrades the L1 Ethereum bridge implementation with a new version, including deployment of new +/// contracts and configuration of administrative roles. +/// +/// # Arguments +/// * `ethereum_bridge_address` - The address of the existing Ethereum bridge contract on L1 +/// * `config_file` - Configuration file containing network and wallet settings +/// +/// # Returns +/// * `Result<()>` - Result indicating success or failure of the upgrade process +/// +/// # Steps +/// 1. Initializes provider and wallet connections using config settings +/// 2. Deploys new bridge implementation and EIC (External Implementation Contract) +/// 3. Sets up proxy connection to existing bridge +/// 4. Performs upgrade sequence: +/// - Adds new implementation to proxy +/// - Upgrades to new implementation +/// - Registers administrative roles (app role admin, governance admin, app governor) +/// - Sets maximum total balance for ETH pub async fn upgrade_l1_bridge(ethereum_bridge_address: Address, config_file: &ConfigFile) -> color_eyre::Result<()> { let config_file = Arc::from(config_file); diff --git a/src/setup_scripts/upgrade_l2_bridge.rs b/src/setup_scripts/upgrade_l2_bridge.rs index 8fd2767..c9d7b7d 100644 --- a/src/setup_scripts/upgrade_l2_bridge.rs +++ b/src/setup_scripts/upgrade_l2_bridge.rs @@ -13,6 +13,23 @@ use crate::utils::constants::{ }; use crate::utils::wait_for_transaction; +/// Upgrades the L2 Ethereum bridge implementation to Cairo 1 through a sequence of contract +/// declarations, deployments, and configuration steps. +/// +/// # Arguments +/// * `account` - The RPC account used to perform the transactions +/// * `rpc_provider_l2` - JSON-RPC client for L2 network communication +/// * `l2_eth_bridge_address` - The address of the existing ETH bridge contract on L2 +/// * `l2_eth_token_address` - The address of the ETH token contract on L2 +/// +/// # Steps +/// 1. Declares and deploys bridge EIC (External Implementation Contract) +/// 2. Declares and deploys new bridge implementation +/// 3. Executes upgrade sequence: +/// - Adds new implementation to proxy with ETH token configuration +/// - Upgrades to new implementation +/// - Registers governance and upgrade administrators +/// - Adds and replaces implementation class hash pub async fn upgrade_eth_bridge_to_cairo_1( account: &RpcAccount<'_>, rpc_provider_l2: &JsonRpcClient, diff --git a/src/tests/mod.rs b/src/tests/mod.rs index afd4c4b..ff145f5 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -2,12 +2,13 @@ pub mod constants; mod erc20_bridge; mod eth_bridge; +use std::future::Future; use std::process::Command; use std::time::Duration; use std::{env, fs}; use rstest::rstest; -use tokio::time::sleep; +use url::Url; use crate::contract_clients::config::Clients; use crate::tests::erc20_bridge::erc20_bridge_test_helper; @@ -30,7 +31,7 @@ async fn test_setup(args: &ConfigFile, clients: &Clients) -> BootstrapperOutput wait_for_madara().await.expect("Failed to start madara!"); // Setup L2 with the updated config - let l2_output = setup_l2(&config, clients).await; + let l2_output = setup_l2(&mut config, clients).await; BootstrapperOutput { starknet_contract_address: Some(core_contract_address), @@ -44,9 +45,9 @@ async fn test_setup(args: &ConfigFile, clients: &Clients) -> BootstrapperOutput #[ignore = "ignored because we have a e2e test, and this is for a local test"] async fn deploy_bridge() -> Result<(), anyhow::Error> { env_logger::init(); - let config = get_test_config_file(); + let mut config = get_test_config_file(); let clients = Clients::init_from_config(&config).await; - bootstrap(&config, &clients).await; + bootstrap(&mut config, &clients).await; Ok(()) } @@ -56,9 +57,9 @@ async fn deploy_bridge() -> Result<(), anyhow::Error> { #[ignore = "ignored because we have a e2e test, and this is for a local test"] async fn deposit_and_withdraw_eth_bridge() -> Result<(), anyhow::Error> { env_logger::init(); - let config = get_test_config_file(); + let mut config = get_test_config_file(); let clients = Clients::init_from_config(&config).await; - let out = bootstrap(&config, &clients).await; + let out = bootstrap(&mut config, &clients).await; let eth_bridge_setup = out.eth_bridge_setup_outputs.unwrap(); let _ = eth_bridge_test_helper( @@ -78,9 +79,9 @@ async fn deposit_and_withdraw_eth_bridge() -> Result<(), anyhow::Error> { #[ignore = "ignored because we have a e2e test, and this is for a local test"] async fn deposit_and_withdraw_erc20_bridge() -> Result<(), anyhow::Error> { env_logger::init(); - let config = get_test_config_file(); + let mut config = get_test_config_file(); let clients = Clients::init_from_config(&config).await; - let out = bootstrap(&config, &clients).await; + let out = bootstrap(&mut config, &clients).await; let eth_token_setup = out.erc20_bridge_setup_outputs.unwrap(); let _ = erc20_bridge_test_helper( @@ -189,8 +190,7 @@ async fn wait_for_madara() -> color_eyre::Result<()> { env::set_current_dir("../").expect("Navigate back failed."); - // Madara build time (approx : 20 mins.) - sleep(Duration::from_secs(1200)).await; + wait_for_madara_to_be_ready(Url::parse("http://localhost:19944")?).await?; Ok(()) } @@ -204,3 +204,43 @@ fn ensure_toolchain() -> color_eyre::Result<()> { } Ok(()) } + +pub async fn wait_for_madara_to_be_ready(rpc_url: Url) -> color_eyre::Result<()> { + // We are fine with `expect` here as this function is called in the intial phases of the + // program execution + let endpoint = rpc_url.join("/health").expect("Request to health endpoint failed"); + // We would wait for about 20-25 mins for madara to be ready + wait_for_cond( + || async { + let res = reqwest::get(endpoint.clone()).await?; + res.error_for_status()?; + Ok(true) + }, + Duration::from_secs(5), + 250, + ) + .await + .expect("Could not get health of Madara"); + Ok(()) +} + +pub async fn wait_for_cond>>( + mut cond: impl FnMut() -> F, + duration: Duration, + attempt_number: usize, +) -> color_eyre::Result { + let mut attempt = 0; + loop { + let err = match cond().await { + Ok(result) => return Ok(result), + Err(err) => err, + }; + + attempt += 1; + if attempt >= attempt_number { + panic!("No answer from the node after {attempt} attempts: {:#}", err) + } + + tokio::time::sleep(duration).await; + } +}