From f2a85be0fb438463cb5f27d1861e5e99aafb907e Mon Sep 17 00:00:00 2001 From: Gabriel de Quadros Ligneul Date: Fri, 3 Jan 2025 16:12:36 -0300 Subject: [PATCH] Add support to Stylus Constructors --- Cargo.lock | 1 + Cargo.toml | 1 + main/Cargo.toml | 1 + main/src/deploy/factory.rs | 192 ++++++++++++++++++++++++++ main/src/{deploy.rs => deploy/mod.rs} | 73 ++++++---- main/src/export_abi.rs | 152 +++++++++++++++++--- main/src/main.rs | 14 ++ 7 files changed, 390 insertions(+), 44 deletions(-) create mode 100644 main/src/deploy/factory.rs rename main/src/{deploy.rs => deploy/mod.rs} (87%) diff --git a/Cargo.lock b/Cargo.lock index a82a2cd..847f387 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1039,6 +1039,7 @@ name = "cargo-stylus" version = "0.5.7" dependencies = [ "alloy-contract", + "alloy-dyn-abi", "alloy-ethers-typecast", "alloy-json-abi", "alloy-primitives 0.7.7", diff --git a/Cargo.toml b/Cargo.toml index ad3cd9e..849369c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ readme = "README.md" [workspace.dependencies] alloy-primitives = "=0.7.7" +alloy-dyn-abi = "=0.7.7" alloy-json-abi = "=0.7.7" alloy-sol-macro = "=0.7.7" alloy-sol-types = "=0.7.7" diff --git a/main/Cargo.toml b/main/Cargo.toml index 3701d43..958f487 100644 --- a/main/Cargo.toml +++ b/main/Cargo.toml @@ -16,6 +16,7 @@ nightly = [] [dependencies] alloy-primitives.workspace = true +alloy-dyn-abi.workspace = true alloy-json-abi.workspace = true alloy-sol-macro.workspace = true alloy-sol-types.workspace = true diff --git a/main/src/deploy/factory.rs b/main/src/deploy/factory.rs new file mode 100644 index 0000000..e14225e --- /dev/null +++ b/main/src/deploy/factory.rs @@ -0,0 +1,192 @@ +// Copyright 2023-2024, Offchain Labs, Inc. +// For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/main/licenses/COPYRIGHT.md + +use super::SignerClient; +use crate::{ + check::ContractCheck, + macros::*, + util::color::{Color, DebugColor}, + DeployConfig, +}; +use alloy_dyn_abi::{DynSolValue, JsonAbiExt, Specifier}; +use alloy_json_abi::{Constructor, StateMutability}; +use alloy_primitives::U256; +use alloy_sol_macro::sol; +use alloy_sol_types::{SolCall, SolEvent}; +use ethers::{ + providers::Middleware, + types::{ + transaction::eip2718::TypedTransaction, Eip1559TransactionRequest, TransactionReceipt, H160, + }, +}; +use eyre::{bail, eyre, Context, Result}; + +sol! { + interface CargoStylusFactory { + event ContractDeployed(address indexed deployedContract, address indexed deployer); + + function deployActivateInit( + bytes calldata bytecode, + bytes calldata constructorCalldata, + uint256 constructorValue + ) public payable returns (address); + + function deployInit( + bytes calldata bytecode, + bytes calldata constructorCalldata + ) public payable returns (address); + } + + function stylus_constructor(); +} + +pub struct FactoryArgs { + /// Factory address + address: H160, + /// Value to be sent in the tx + tx_value: U256, + /// Calldata to be sent in the tx + tx_calldata: Vec, +} + +/// Parses the constructor arguments and returns the data to deploy the contract using the factory. +pub fn parse_constructor_args( + cfg: &DeployConfig, + constructor: &Constructor, + contract: &ContractCheck, +) -> Result { + let Some(address) = cfg.experimental_factory_address else { + bail!("missing factory address"); + }; + + let constructor_value = + alloy_ethers_typecast::ethers_u256_to_alloy(cfg.experimental_constructor_value); + if constructor.state_mutability != StateMutability::Payable && !constructor_value.is_zero() { + bail!("attempting to send Ether to non-payable constructor"); + } + let tx_value = contract.suggest_fee() + constructor_value; + + let args = &cfg.experimental_constructor_args; + let params = &constructor.inputs; + if args.len() != params.len() { + bail!( + "mismatch number of constructor arguments (want {}; got {})", + params.len(), + args.len() + ); + } + + let mut arg_values = Vec::::with_capacity(args.len()); + for (arg, param) in args.iter().zip(params) { + let ty = param + .resolve() + .wrap_err_with(|| format!("could not resolve constructor arg: {param}"))?; + let value = ty + .coerce_str(arg) + .wrap_err_with(|| format!("could not parse constructor arg: {param}"))?; + arg_values.push(value); + } + let calldata_args = constructor.abi_encode_input_raw(&arg_values)?; + + let mut constructor_calldata = Vec::from(stylus_constructorCall::SELECTOR); + constructor_calldata.extend(calldata_args); + + let bytecode = super::contract_deployment_calldata(contract.code()); + let tx_calldata = if contract.suggest_fee().is_zero() { + CargoStylusFactory::deployInitCall { + bytecode: bytecode.into(), + constructorCalldata: constructor_calldata.into(), + } + .abi_encode() + } else { + CargoStylusFactory::deployActivateInitCall { + bytecode: bytecode.into(), + constructorCalldata: constructor_calldata.into(), + constructorValue: constructor_value, + } + .abi_encode() + }; + + Ok(FactoryArgs { + address, + tx_value, + tx_calldata, + }) +} + +/// Deploys, activates, and initializes the contract using the Stylus factory. +pub async fn deploy( + cfg: &DeployConfig, + factory: FactoryArgs, + sender: H160, + client: &SignerClient, +) -> Result<()> { + if cfg.check_config.common_cfg.verbose { + greyln!( + "deploying contract using factory at address: {}", + factory.address.debug_lavender() + ); + } + + let tx = Eip1559TransactionRequest::new() + .to(factory.address) + .from(sender) + .value(alloy_ethers_typecast::alloy_u256_to_ethers( + factory.tx_value, + )) + .data(factory.tx_calldata); + + let gas = client + .estimate_gas(&TypedTransaction::Eip1559(tx.clone()), None) + .await?; + if cfg.check_config.common_cfg.verbose || cfg.estimate_gas { + super::print_gas_estimate("factory deploy, activate, and init", client, gas).await?; + } + if cfg.estimate_gas { + return Ok(()); + } + + let receipt = super::run_tx( + "deploy_activate_init", + tx, + Some(gas), + cfg.check_config.common_cfg.max_fee_per_gas_gwei, + client, + cfg.check_config.common_cfg.verbose, + ) + .await?; + let contract = get_address_from_receipt(&receipt)?; + let address = contract.debug_lavender(); + + if cfg.check_config.common_cfg.verbose { + let gas = super::format_gas(receipt.gas_used.unwrap_or_default()); + greyln!( + "deployed code at address: {address} {} {gas}", + "with".grey() + ); + } else { + greyln!("deployed code at address: {address}"); + } + let tx_hash = receipt.transaction_hash.debug_lavender(); + greyln!("deployment tx hash: {tx_hash}"); + super::print_cache_notice(contract); + Ok(()) +} + +/// Gets the Stylus-contract address that was deployed using the factory. +fn get_address_from_receipt(receipt: &TransactionReceipt) -> Result { + for log in receipt.logs.iter() { + if let Some(topic) = log.topics.first() { + if topic.0 == CargoStylusFactory::ContractDeployed::SIGNATURE_HASH { + let address = log + .topics + .get(1) + .ok_or(eyre!("address missing from ContractDeployed log"))?; + return Ok(ethers::types::Address::from_slice( + &address.as_bytes()[12..32], + )); + } + } + } + Err(eyre!("contract address not found in receipt")) +} diff --git a/main/src/deploy.rs b/main/src/deploy/mod.rs similarity index 87% rename from main/src/deploy.rs rename to main/src/deploy/mod.rs index 7ab0f90..749a6a5 100644 --- a/main/src/deploy.rs +++ b/main/src/deploy/mod.rs @@ -2,14 +2,15 @@ // For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/main/licenses/COPYRIGHT.md #![allow(clippy::println_empty_string)] -use crate::util::{ - color::{Color, DebugColor}, - sys, -}; use crate::{ check::{self, ContractCheck}, constants::ARB_WASM_H160, + export_abi, macros::*, + util::{ + color::{Color, DebugColor}, + sys, + }, DeployConfig, }; use alloy_primitives::{Address, U256 as AU256}; @@ -26,6 +27,8 @@ use ethers::{ }; use eyre::{bail, eyre, Result, WrapErr}; +mod factory; + sol! { interface ArbWasm { function activateProgram(address program) @@ -44,6 +47,11 @@ pub async fn deploy(cfg: DeployConfig) -> Result<()> { .expect("cargo stylus check failed"); let verbose = cfg.check_config.common_cfg.verbose; + let constructor = export_abi::get_constructor_signature()?; + let factory_args = constructor + .map(|constructor| factory::parse_constructor_args(&cfg, &constructor, &contract)) + .transpose()?; + let client = sys::new_provider(&cfg.check_config.common_cfg.endpoint)?; let chain_id = client.get_chainid().await.expect("failed to get chain id"); @@ -56,7 +64,8 @@ pub async fn deploy(cfg: DeployConfig) -> Result<()> { greyln!("sender address: {}", sender.debug_lavender()); } - let data_fee = contract.suggest_fee(); + let data_fee = contract.suggest_fee() + + alloy_ethers_typecast::ethers_u256_to_alloy(cfg.experimental_constructor_value); if let ContractCheck::Ready { .. } = &contract { // check balance early @@ -79,6 +88,10 @@ pub async fn deploy(cfg: DeployConfig) -> Result<()> { } } + if let Some(factory_args) = factory_args { + return factory::deploy(&cfg, factory_args, sender, &client).await; + } + let contract_addr = cfg .deploy_contract(contract.code(), sender, &client) .await?; @@ -102,13 +115,7 @@ cargo stylus activate --address {}"#, } ContractCheck::Active { .. } => greyln!("wasm already activated!"), } - println!(""); - let contract_addr = hex::encode(contract_addr); - mintln!( - r#"NOTE: We recommend running cargo stylus cache bid {contract_addr} 0 to cache your activated contract in ArbOS. -Cached contracts benefit from cheaper calls. To read more about the Stylus contract cache, see -https://docs.arbitrum.io/stylus/concepts/stylus-cache-manager"# - ); + print_cache_notice(contract_addr); Ok(()) } @@ -131,19 +138,7 @@ impl DeployConfig { .await?; if self.check_config.common_cfg.verbose || self.estimate_gas { - let gas_price = client.get_gas_price().await?; - greyln!("estimates"); - greyln!("deployment tx gas: {}", gas.debug_lavender()); - greyln!( - "gas price: {} gwei", - format_units(gas_price, "gwei")?.debug_lavender() - ); - let total_cost = gas_price.checked_mul(gas).unwrap_or_default(); - let eth_estimate = format_units(total_cost, "ether")?; - greyln!( - "deployment tx total cost: {} ETH", - eth_estimate.debug_lavender() - ); + print_gas_estimate("deployment", client, gas).await?; } if self.estimate_gas { let nonce = client.get_transaction_count(sender, None).await?; @@ -229,6 +224,34 @@ impl DeployConfig { } } +pub async fn print_gas_estimate(name: &str, client: &SignerClient, gas: U256) -> Result<()> { + let gas_price = client.get_gas_price().await?; + greyln!("estimates"); + greyln!("{} tx gas: {}", name, gas.debug_lavender()); + greyln!( + "gas price: {} gwei", + format_units(gas_price, "gwei")?.debug_lavender() + ); + let total_cost = gas_price.checked_mul(gas).unwrap_or_default(); + let eth_estimate = format_units(total_cost, "ether")?; + greyln!( + "{} tx total cost: {} ETH", + name, + eth_estimate.debug_lavender() + ); + Ok(()) +} + +pub fn print_cache_notice(contract_addr: H160) { + let contract_addr = hex::encode(contract_addr); + println!(""); + mintln!( + r#"NOTE: We recommend running cargo stylus cache bid {contract_addr} 0 to cache your activated contract in ArbOS. +Cached contracts benefit from cheaper calls. To read more about the Stylus contract cache, see +https://docs.arbitrum.io/stylus/concepts/stylus-cache-manager"# + ); +} + pub async fn run_tx( name: &str, tx: Eip1559TransactionRequest, diff --git a/main/src/export_abi.rs b/main/src/export_abi.rs index e6d5ba5..3de96b5 100644 --- a/main/src/export_abi.rs +++ b/main/src/export_abi.rs @@ -3,6 +3,7 @@ use crate::macros::*; use crate::util::{color::Color, sys}; +use alloy_json_abi::Constructor; use eyre::{bail, Result, WrapErr}; use std::{ io::Write, @@ -17,22 +18,7 @@ pub fn export_abi(file: Option, json: bool) -> Result<()> { bail!("solc not found. Please see\n{link}"); } - let target = format!("--target={}", sys::host_arch()?); - let mut output = Command::new("cargo") - .stderr(Stdio::inherit()) - .arg("run") - .arg("--features=export-abi") - .arg(target) - .output()?; - - if !output.status.success() { - let out = String::from_utf8_lossy(&output.stdout); - let out = (out != "") - .then_some(format!(": {out}")) - .unwrap_or_default(); - egreyln!("failed to run contract {out}"); - process::exit(1); - } + let mut output = run_export("abi")?; // convert the ABI to a JSON file via solc if json { @@ -46,11 +32,139 @@ pub fn export_abi(file: Option, json: bool) -> Result<()> { .wrap_err("failed to run solc")?; let mut stdin = solc.stdin.as_ref().unwrap(); - stdin.write_all(&output.stdout)?; - output = solc.wait_with_output()?; + stdin.write_all(&output)?; + output = solc.wait_with_output()?.stdout; } let mut out = sys::file_or_stdout(file)?; - out.write_all(&output.stdout)?; + out.write_all(&output)?; Ok(()) } + +/// Gets the constructor signature of the Stylus contract using the export binary. +/// If the contract doesn't have a constructor, returns None. +pub fn get_constructor_signature() -> Result> { + let output = run_export("constructor")?; + let output = String::from_utf8(output)?; + parse_constructor(&output) +} + +fn run_export(command: &str) -> Result> { + let target = format!("--target={}", sys::host_arch()?); + let output = Command::new("cargo") + .stderr(Stdio::inherit()) + .arg("run") + .arg("--quiet") + .arg("--features=export-abi") + .arg(target) + .arg("--") + .arg(command) + .output()?; + if !output.status.success() { + let out = String::from_utf8_lossy(&output.stdout); + let out = (out != "") + .then_some(format!(": {out}")) + .unwrap_or_default(); + egreyln!("failed to run contract {out}"); + process::exit(1); + } + Ok(output.stdout) +} + +fn parse_constructor(signature: &str) -> Result> { + let signature = signature.trim(); + if !signature.starts_with("constructor") { + // If the signature doesn't start with constructor, it is either an old SDK version that + // doesn't support it or the contract doesn't have one. So, it is safe to return None. + Ok(None) + } else { + Constructor::parse(signature) + .map(Some) + .map_err(|e| e.into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_json_abi::Param; + + #[test] + fn parse_constructors() { + let test_cases = vec![ + ( + "constructor()", + Some(Constructor { + inputs: vec![], + state_mutability: alloy_json_abi::StateMutability::NonPayable, + }), + ), + ( + "constructor(uint256 foo)", + Some(Constructor { + inputs: vec![Param { + ty: "uint256".to_owned(), + name: "foo".to_owned(), + components: vec![], + internal_type: None, + }], + state_mutability: alloy_json_abi::StateMutability::NonPayable, + }), + ), + ( + "constructor((uint256, uint256) foo, uint8[] memory arr) payable", + Some(Constructor { + inputs: vec![ + Param { + ty: "tuple".to_owned(), + name: "foo".to_owned(), + components: vec![ + Param { + ty: "uint256".to_owned(), + name: "".to_owned(), + components: vec![], + internal_type: None, + }, + Param { + ty: "uint256".to_owned(), + name: "".to_owned(), + components: vec![], + internal_type: None, + }, + ], + internal_type: None, + }, + Param { + ty: "uint8[]".to_owned(), + name: "arr".to_owned(), + components: vec![], + internal_type: None, + }, + ], + state_mutability: alloy_json_abi::StateMutability::Payable, + }), + ), + ("", None), + ( + "/** + * This file was automatically generated by Stylus and represents a Rust program. + * For more information, please see [The Stylus SDK](https://github.com/OffchainLabs/stylus-sdk-rs). + */ + +// SPDX-License-Identifier: MIT-OR-APACHE-2.0 +pragma solidity ^0.8.23; + +interface ICounter { + function number() external view returns (uint256); + + function setNumber(uint256 new_number) external; +}", + None, + ), + ]; + for (signature, expected) in test_cases { + let constructor = parse_constructor(signature).expect("failed to parse"); + assert_eq!(constructor, expected); + } + } +} diff --git a/main/src/main.rs b/main/src/main.rs index c5e648e..ff14603 100644 --- a/main/src/main.rs +++ b/main/src/main.rs @@ -235,6 +235,20 @@ struct DeployConfig { /// If set, do not activate the program after deploying it #[arg(long)] no_activate: bool, + /// The address of the factory contract that deploys, activates, and executes the stylus constructor. + #[arg(long, value_name = "FACTORY_ADDRESS")] + experimental_factory_address: Option, + /// The constructor arguments. + #[arg( + long, + num_args(0..), + value_name = "ARGS", + allow_hyphen_values = true, + )] + experimental_constructor_args: Vec, + /// The amount of Ether sent to the contract through the constructor. + #[arg(long, default_value = "0")] + experimental_constructor_value: U256, } #[derive(Args, Clone, Debug)]