Skip to content

Commit

Permalink
Add support to Stylus Constructors
Browse files Browse the repository at this point in the history
  • Loading branch information
gligneul committed Jan 8, 2025
1 parent a56a4f3 commit f2a85be
Show file tree
Hide file tree
Showing 7 changed files with 390 additions and 44 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions main/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
192 changes: 192 additions & 0 deletions main/src/deploy/factory.rs
Original file line number Diff line number Diff line change
@@ -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<u8>,
}

/// 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<FactoryArgs> {
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::<DynSolValue>::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<H160> {
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"))
}
73 changes: 48 additions & 25 deletions main/src/deploy.rs → main/src/deploy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -26,6 +27,8 @@ use ethers::{
};
use eyre::{bail, eyre, Result, WrapErr};

mod factory;

sol! {
interface ArbWasm {
function activateProgram(address program)
Expand All @@ -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");

Expand All @@ -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
Expand All @@ -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?;
Expand All @@ -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(())
}

Expand All @@ -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?;
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit f2a85be

Please sign in to comment.