Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to Stylus Constructors #131

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading