diff --git a/Cargo.lock b/Cargo.lock index 2b8bcfc..22fada3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1039,7 +1039,6 @@ name = "cargo-stylus" version = "0.5.8" 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 1f383c2..a751677 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,6 @@ 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 958f487..3701d43 100644 --- a/main/Cargo.toml +++ b/main/Cargo.toml @@ -16,7 +16,6 @@ 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/cache.rs b/main/src/cache.rs index b2798bd..d1a7694 100644 --- a/main/src/cache.rs +++ b/main/src/cache.rs @@ -11,9 +11,8 @@ use eyre::{bail, Result}; use CacheManager::CacheManagerErrors; use crate::constants::ARB_WASM_CACHE_ADDRESS; -use crate::deploy::gwei_to_wei; use crate::macros::greyln; -use crate::{CacheBidConfig, CacheStatusConfig, CacheSuggestionsConfig}; +use crate::{CacheBidConfig, CacheStatusConfig, CacheSuggestionsConfig, GasFeeConfig}; sol! { #[sol(rpc)] @@ -178,8 +177,9 @@ pub async fn place_bid(cfg: &CacheBidConfig) -> Result<()> { let cache_manager = CacheManager::new(cache_manager_addr, provider.clone()); let addr = cfg.address.to_fixed_bytes().into(); let mut place_bid_call = cache_manager.placeBid(addr).value(U256::from(cfg.bid)); - if let Some(max_fee) = cfg.max_fee_per_gas_gwei { - place_bid_call = place_bid_call.max_fee_per_gas(gwei_to_wei(max_fee)?); + if let Some(max_fee) = cfg.get_max_fee_per_gas_wei()? { + place_bid_call = place_bid_call.max_fee_per_gas(max_fee); + place_bid_call = place_bid_call.max_priority_fee_per_gas(0); }; greyln!("Checking if contract can be cached..."); diff --git a/main/src/deploy/mod.rs b/main/src/deploy.rs similarity index 83% rename from main/src/deploy/mod.rs rename to main/src/deploy.rs index f1c1d2e..165c1e6 100644 --- a/main/src/deploy/mod.rs +++ b/main/src/deploy.rs @@ -5,13 +5,15 @@ use crate::{ check::{self, ContractCheck}, constants::ARB_WASM_H160, - export_abi, macros::*, + DeployConfig, +}; +use crate::{ util::{ color::{Color, DebugColor}, sys, }, - DeployConfig, + GasFeeConfig, }; use alloy_primitives::{Address, U256 as AU256}; use alloy_sol_macro::sol; @@ -27,8 +29,6 @@ use ethers::{ }; use eyre::{bail, eyre, Result, WrapErr}; -mod deployer; - sol! { interface ArbWasm { function activateProgram(address program) @@ -47,11 +47,6 @@ 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 deployer_args = constructor - .map(|constructor| deployer::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"); @@ -64,8 +59,7 @@ pub async fn deploy(cfg: DeployConfig) -> Result<()> { greyln!("sender address: {}", sender.debug_lavender()); } - let data_fee = contract.suggest_fee() - + alloy_ethers_typecast::ethers_u256_to_alloy(cfg.experimental_constructor_value); + let data_fee = contract.suggest_fee(); if let ContractCheck::Ready { .. } = &contract { // check balance early @@ -88,10 +82,6 @@ pub async fn deploy(cfg: DeployConfig) -> Result<()> { } } - if let Some(deployer_args) = deployer_args { - return deployer::deploy(&cfg, deployer_args, sender, &client).await; - } - let contract_addr = cfg .deploy_contract(contract.code(), sender, &client) .await?; @@ -115,7 +105,13 @@ cargo stylus activate --address {}"#, } ContractCheck::Active { .. } => greyln!("wasm already activated!"), } - print_cache_notice(contract_addr); + 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"# + ); Ok(()) } @@ -136,20 +132,37 @@ impl DeployConfig { let gas = client .estimate_gas(&TypedTransaction::Eip1559(tx.clone()), None) .await?; + let gas_price = client.get_gas_price().await?; if self.check_config.common_cfg.verbose || self.estimate_gas { - print_gas_estimate("deployment", client, gas).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() + ); } if self.estimate_gas { let nonce = client.get_transaction_count(sender, None).await?; return Ok(ethers::utils::get_contract_address(sender, nonce)); } + let fee_per_gas = match self.check_config.common_cfg.get_max_fee_per_gas_wei()? { + Some(wei) => wei, + None => gas_price.try_into().unwrap_or_default(), + }; + let receipt = run_tx( "deploy", tx, Some(gas), - self.check_config.common_cfg.max_fee_per_gas_gwei, + fee_per_gas, client, self.check_config.common_cfg.verbose, ) @@ -198,15 +211,22 @@ impl DeployConfig { .await .map_err(|e| eyre!("did not estimate correctly: {e}"))?; + let gas_price = client.get_gas_price().await?; + if self.check_config.common_cfg.verbose || self.estimate_gas { greyln!("activation gas estimate: {}", format_gas(gas)); } + let fee_per_gas = match self.check_config.common_cfg.get_max_fee_per_gas_wei()? { + Some(wei) => wei, + None => gas_price.try_into().unwrap_or_default(), + }; + let receipt = run_tx( "activate", tx, Some(gas), - self.check_config.common_cfg.max_fee_per_gas_gwei, + fee_per_gas, client, self.check_config.common_cfg.verbose, ) @@ -224,39 +244,11 @@ 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, gas: Option, - max_fee_per_gas_gwei: Option, + max_fee_per_gas_wei: u128, client: &SignerClient, verbose: bool, ) -> Result { @@ -264,9 +256,10 @@ pub async fn run_tx( if let Some(gas) = gas { tx.gas = Some(gas); } - if let Some(max_fee) = max_fee_per_gas_gwei { - tx.max_fee_per_gas = Some(U256::from(gwei_to_wei(max_fee)?)); - } + + tx.max_fee_per_gas = Some(U256::from(max_fee_per_gas_wei)); + tx.max_priority_fee_per_gas = Some(U256::from(0)); + let tx = TypedTransaction::Eip1559(tx); let tx = client.send_transaction(tx, None).await?; let tx_hash = tx.tx_hash(); @@ -328,11 +321,3 @@ pub fn format_gas(gas: U256) -> String { text.pink() } } - -pub fn gwei_to_wei(gwei: u128) -> Result { - let wei_per_gwei: u128 = 10u128.pow(9); - match gwei.checked_mul(wei_per_gwei) { - Some(wei) => Ok(wei), - None => bail!("overflow occurred while converting gwei to wei"), - } -} diff --git a/main/src/deploy/deployer.rs b/main/src/deploy/deployer.rs deleted file mode 100644 index baa1825..0000000 --- a/main/src/deploy/deployer.rs +++ /dev/null @@ -1,186 +0,0 @@ -// 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, GREY}, - 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, - }, - utils::format_ether, -}; -use eyre::{bail, eyre, Context, Result}; - -sol! { - interface StylusDeployer { - event ContractDeployed(address deployedContract); - - function deploy( - bytes calldata bytecode, - bytes calldata initData, - uint256 initValue, - bytes32 salt - ) public payable returns (address); - } - - function stylus_constructor(); -} - -pub struct DeployerArgs { - /// 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 deployer. -pub fn parse_constructor_args( - cfg: &DeployConfig, - constructor: &Constructor, - contract: &ContractCheck, -) -> Result { - let Some(address) = cfg.experimental_deployer_address else { - bail!("this contract has a constructor so it requires the deployer address for deployment"); - }; - - if !cfg.experimental_constructor_value.is_zero() { - greyln!( - "value sent to the constructor: {} {GREY}Ether", - format_ether(cfg.experimental_constructor_value).debug_lavender() - ); - } - 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 = StylusDeployer::deployCall { - bytecode: bytecode.into(), - initData: constructor_calldata.into(), - initValue: constructor_value, - salt: cfg.experimental_deployer_salt, - } - .abi_encode(); - - Ok(DeployerArgs { - address, - tx_value, - tx_calldata, - }) -} - -/// Deploys, activates, and initializes the contract using the Stylus deployer. -pub async fn deploy( - cfg: &DeployConfig, - deployer: DeployerArgs, - sender: H160, - client: &SignerClient, -) -> Result<()> { - if cfg.check_config.common_cfg.verbose { - greyln!( - "deploying contract using deployer at address: {}", - deployer.address.debug_lavender() - ); - } - - let tx = Eip1559TransactionRequest::new() - .to(deployer.address) - .from(sender) - .value(alloy_ethers_typecast::alloy_u256_to_ethers( - deployer.tx_value, - )) - .data(deployer.tx_calldata); - - let gas = client - .estimate_gas(&TypedTransaction::Eip1559(tx.clone()), None) - .await - .wrap_err("deployment failed during gas estimation")?; - if cfg.check_config.common_cfg.verbose || cfg.estimate_gas { - super::print_gas_estimate("deployer 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 deployer. -fn get_address_from_receipt(receipt: &TransactionReceipt) -> Result { - for log in receipt.logs.iter() { - if let Some(topic) = log.topics.first() { - if topic.0 == StylusDeployer::ContractDeployed::SIGNATURE_HASH { - if log.data.len() != 32 { - bail!("address missing from ContractDeployed log"); - } - return Ok(ethers::types::Address::from_slice(&log.data[12..32])); - } - } - } - Err(eyre!("contract address not found in receipt")) -} diff --git a/main/src/export_abi.rs b/main/src/export_abi.rs index 3de96b5..e6d5ba5 100644 --- a/main/src/export_abi.rs +++ b/main/src/export_abi.rs @@ -3,7 +3,6 @@ use crate::macros::*; use crate::util::{color::Color, sys}; -use alloy_json_abi::Constructor; use eyre::{bail, Result, WrapErr}; use std::{ io::Write, @@ -18,48 +17,14 @@ pub fn export_abi(file: Option, json: bool) -> Result<()> { bail!("solc not found. Please see\n{link}"); } - let mut output = run_export("abi")?; - - // convert the ABI to a JSON file via solc - if json { - let solc = Command::new("solc") - .stdin(Stdio::piped()) - .stderr(Stdio::inherit()) - .stdout(Stdio::piped()) - .arg("--abi") - .arg("-") - .spawn() - .wrap_err("failed to run solc")?; - - let mut stdin = solc.stdin.as_ref().unwrap(); - stdin.write_all(&output)?; - output = solc.wait_with_output()?.stdout; - } - - let mut out = sys::file_or_stdout(file)?; - 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") + let mut 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 != "") @@ -68,103 +33,24 @@ fn run_export(command: &str) -> Result> { 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); + // convert the ABI to a JSON file via solc + if json { + let solc = Command::new("solc") + .stdin(Stdio::piped()) + .stderr(Stdio::inherit()) + .stdout(Stdio::piped()) + .arg("--abi") + .arg("-") + .spawn() + .wrap_err("failed to run solc")?; - 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); - } + let mut stdin = solc.stdin.as_ref().unwrap(); + stdin.write_all(&output.stdout)?; + output = solc.wait_with_output()?; } + + let mut out = sys::file_or_stdout(file)?; + out.write_all(&output.stdout)?; + Ok(()) } diff --git a/main/src/main.rs b/main/src/main.rs index 0d9bc8f..5993c76 100644 --- a/main/src/main.rs +++ b/main/src/main.rs @@ -4,7 +4,7 @@ // Enable unstable test feature for benchmarks when nightly is available #![cfg_attr(feature = "nightly", feature(test))] -use alloy_primitives::{TxHash, B256}; +use alloy_primitives::TxHash; use clap::{ArgGroup, Args, CommandFactory, Parser, Subcommand}; use constants::DEFAULT_ENDPOINT; use ethers::abi::Bytes; @@ -125,7 +125,7 @@ struct CommonConfig { source_files_for_project_hash: Vec, #[arg(long)] /// Optional max fee per gas in gwei units. - max_fee_per_gas_gwei: Option, + max_fee_per_gas_gwei: Option, /// Specifies the features to use when building the Stylus binary. #[arg(long)] features: Option, @@ -161,7 +161,7 @@ pub struct CacheBidConfig { bid: u64, #[arg(long)] /// Optional max fee per gas in gwei units. - max_fee_per_gas_gwei: Option, + max_fee_per_gas_gwei: Option, } #[derive(Args, Clone, Debug)] @@ -235,23 +235,6 @@ struct DeployConfig { /// If set, do not activate the program after deploying it #[arg(long)] no_activate: bool, - /// The address of the deployer contract that deploys, activates, and initializes the stylus constructor. - #[arg(long, value_name = "DEPLOYER_ADDRESS")] - experimental_deployer_address: Option, - /// The salt passed to the stylus deployer. - #[arg(long, default_value_t = B256::ZERO)] - experimental_deployer_salt: B256, - /// 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, value_parser = parse_ether, default_value = "0")] - experimental_constructor_value: U256, } #[derive(Args, Clone, Debug)] @@ -368,6 +351,60 @@ struct AuthOpts { keystore_password_path: Option, } +pub trait GasFeeConfig { + fn get_max_fee_per_gas_wei(&self) -> Result>; + fn get_fee_str(&self) -> &Option; +} + +fn convert_gwei_to_wei(fee_str: &str) -> Result { + let gwei = match fee_str.parse::() { + Ok(fee) if fee >= 0.0 => fee, + Ok(_) => bail!("Max fee per gas must be non-negative"), + Err(_) => bail!("Invalid max fee per gas value: {}", fee_str), + }; + + if !gwei.is_finite() { + bail!("Invalid gwei value: must be finite"); + } + + let wei = gwei * 1e9; + if !wei.is_finite() { + bail!("Overflow occurred in floating point multiplication"); + } + + if wei < 0.0 || wei >= u128::MAX as f64 { + bail!("Result outside valid range for wei"); + } + + Ok(wei as u128) +} + +impl GasFeeConfig for CommonConfig { + fn get_fee_str(&self) -> &Option { + &self.max_fee_per_gas_gwei + } + + fn get_max_fee_per_gas_wei(&self) -> Result> { + match self.get_fee_str() { + Some(fee_str) => Ok(Some(convert_gwei_to_wei(fee_str)?)), + None => Ok(None), + } + } +} + +impl GasFeeConfig for CacheBidConfig { + fn get_fee_str(&self) -> &Option { + &self.max_fee_per_gas_gwei + } + + fn get_max_fee_per_gas_wei(&self) -> Result> { + match self.get_fee_str() { + Some(fee_str) => Ok(Some(convert_gwei_to_wei(fee_str)?)), + None => Ok(None), + } + } +} + impl fmt::Display for CommonConfig { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // Convert the vector of source files to a comma-separated string @@ -784,7 +821,3 @@ pub fn find_shared_library(project: &Path, extension: &str) -> Result { }; Ok(file) } - -fn parse_ether(s: &str) -> Result { - Ok(ethers::utils::parse_ether(s)?) -}