Skip to content

Commit

Permalink
feat: add support for cast estimate (#903)
Browse files Browse the repository at this point in the history
  • Loading branch information
elfedy authored Feb 10, 2025
1 parent 03a8f73 commit 5eaf1a7
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 3 deletions.
24 changes: 21 additions & 3 deletions crates/cast/bin/cmd/estimate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ use foundry_cli::{
use foundry_common::ens::NameOrAddress;
use std::str::FromStr;

mod zksync;

/// CLI arguments for `cast estimate`.
#[derive(Debug, Parser)]
pub struct EstimateArgs {
Expand Down Expand Up @@ -38,6 +40,14 @@ pub struct EstimateArgs {

#[command(flatten)]
eth: EthereumOpts,

/// Zksync Transaction
#[command(flatten)]
zk_tx: zksync::ZkTransactionOpts,

/// Force a zksync eip-712 transaction and apply CREATE overrides
#[arg(long = "zksync")]
zk_force: bool,
}

#[derive(Debug, Parser)]
Expand Down Expand Up @@ -66,7 +76,7 @@ pub enum EstimateSubcommands {

impl EstimateArgs {
pub async fn run(self) -> Result<()> {
let Self { to, mut sig, mut args, mut tx, block, eth, command } = self;
let Self { to, mut sig, mut args, mut tx, block, eth, command, zk_tx, zk_force } = self;

let config = eth.load_config()?;
let provider = utils::get_provider(&config)?;
Expand All @@ -93,12 +103,20 @@ impl EstimateArgs {
.await?
.with_to(to)
.await?
.with_code_sig_and_args(code, sig, args)
// NOTE(zk): `with_code_sig_and_args` decodes the code and appends it to the input
// we want the raw decoded constructor input from that function so we keep the code
// to encode the CONTRACT_CREATOR call later
.with_code_sig_and_args(code.clone(), sig, args)
.await?
.build_raw(sender)
.await?;

let gas = provider.estimate_gas(&tx).block(block.unwrap_or_default()).await?;
let gas = if zk_tx.has_zksync_args() || zk_force {
zksync::estimate_gas(zk_tx, &config, tx, code).await?
} else {
provider.estimate_gas(&tx).block(block.unwrap_or_default()).await?
};

sh_println!("{gas}")?;
Ok(())
}
Expand Down
93 changes: 93 additions & 0 deletions crates/cast/bin/cmd/estimate/zksync.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
use alloy_network::TransactionBuilder;
use alloy_primitives::{hex, Address, Bytes, TxKind, U256};
use alloy_provider::Provider;
use alloy_rpc_types::TransactionRequest;
use alloy_serde::WithOtherFields;
use alloy_zksync::network::{
transaction_request::TransactionRequest as ZkTransactionRequest,
unsigned_tx::eip712::PaymasterParams,
};
use clap::{command, Parser};
use eyre::Result;
use foundry_cli::utils;
use foundry_config::Config;

#[derive(Clone, Debug, Parser)]
#[command(next_help_heading = "Transaction options")]
pub struct ZkTransactionOpts {
/// Paymaster address for the ZKSync transaction
#[arg(long = "zk-paymaster-address", requires = "paymaster_input")]
pub paymaster_address: Option<Address>,

/// Paymaster input for the ZKSync transaction
#[arg(long = "zk-paymaster-input", requires = "paymaster_address", value_parser = parse_hex_bytes)]
pub paymaster_input: Option<Bytes>,

/// Factory dependencies for the ZKSync transaction
#[arg(long = "zk-factory-deps", value_parser = parse_hex_bytes, value_delimiter = ',')]
pub factory_deps: Vec<Bytes>,

// TODO: fix custom signature serialization and then add this field
// /// Custom signature for the ZKSync transaction
// #[arg(long = "zk-custom-signature", value_parser = parse_hex_bytes)]
// pub custom_signature: Option<Bytes>,
/// Gas per pubdata for the ZKSync transaction
#[arg(long = "zk-gas-per-pubdata")]
pub gas_per_pubdata: Option<U256>,
}

fn parse_hex_bytes(s: &str) -> Result<Bytes, String> {
hex::decode(s).map(Bytes::from).map_err(|e| format!("Invalid hex string: {e}"))
}

impl ZkTransactionOpts {
pub fn has_zksync_args(&self) -> bool {
self.paymaster_address.is_some()
|| !self.factory_deps.is_empty()
// TODO: add this when fixing serialization || self.custom_signature.is_some()
|| self.gas_per_pubdata.is_some()
}
}

pub async fn estimate_gas(
zk_tx: ZkTransactionOpts,
config: &Config,
evm_tx: WithOtherFields<TransactionRequest>,
zk_code: Option<String>,
) -> Result<u64> {
let zk_provider = utils::get_provider_zksync(config)?;
let is_create = evm_tx.to == Some(TxKind::Create);
let mut tx: ZkTransactionRequest = evm_tx.inner.clone().into();
if let Some(gas_per_pubdata) = zk_tx.gas_per_pubdata {
tx.set_gas_per_pubdata(gas_per_pubdata)
}

// TODO: Fix custom_signature serialization and then add this field
// if let Some(custom_signature) = &zk_tx.custom_signature {
// tx.set_custom_signature(custom_signature.clone());
// }

if let (Some(paymaster), Some(paymaster_input)) =
(zk_tx.paymaster_address, zk_tx.paymaster_input.clone())
{
tx.set_paymaster_params(PaymasterParams { paymaster, paymaster_input });
}

if is_create {
let evm_input: Vec<u8> = tx.input().cloned().map(|bytes| bytes.into()).unwrap_or_default();
let zk_code_decoded = hex::decode(zk_code.unwrap_or_default())?;
// constructor input gets appended to the bytecode
let zk_input = &evm_input[zk_code_decoded.len()..];
tx = tx.with_create_params(
zk_code_decoded,
zk_input.to_vec(),
zk_tx.factory_deps.into_iter().map(|v| v.into()).collect(),
)?;
} else {
tx.set_factory_deps(zk_tx.factory_deps.clone());
}

// TODO: Check if alloy calls this for estimate_gas. If so, we do not need this.
tx.prep_for_submission();
Ok(zk_provider.estimate_gas(&tx).await?)
}
72 changes: 72 additions & 0 deletions crates/cast/tests/cli/zk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,29 @@ casttest!(test_zk_cast_using_paymaster, async |_prj, cmd| {
.get_output()
.stdout_lossy();

// Test cast estimate with paymaster params
let output = cmd
.cast_fuse()
.args([
"estimate",
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"increment()",
"--private-key",
&private_key,
"--rpc-url",
&url,
"--zk-paymaster-address",
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"--zk-paymaster-input",
"0x8c5a344500000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000",
])
.assert_success()
.get_output()
.stdout_lossy();

let output: u32 = output.trim().parse().unwrap();
assert!(output > 0);

// Interact with the counter using the paymaster
cmd.cast_fuse().args([
"send",
Expand Down Expand Up @@ -136,3 +159,52 @@ casttest!(test_zk_cast_without_paymaster, async |_prj, cmd| {

assert!(balance_after != balance_before);
});

// tests that `cast estimate --create` is working correctly.
casttest!(test_zk_cast_estimate_contract_deploy_gas, async |_prj, cmd| {
let node = ZkSyncNode::start().await;
let url = node.url();

let addr = "0x36615Cf349d7F6344891B1e7CA7C72883F5dc049";

let output = cmd
.args([
"estimate",
"--rpc-url",
&url,
"--from",
addr,
"--zksync",
"--create",
COUNTER_BYTECODE,
])
.assert_success()
.get_output()
.stdout_lossy();

let output: u32 = output.trim().parse().unwrap();
assert!(output > 0);

// empty contract with constructor(uint256) function
let bytecode_with_constructor = "000000600310027000000010033001970000000100200190000000130000c13d0000008002000039000000400020043f000000040030008c000000330000413d000000000101043b0000001401100197000000150010009c000000330000c13d0000000001000416000000000001004b000000330000c13d000000000100041a000000800010043f00000016010000410000003d0001042e0000000002000416000000000002004b000000330000c13d0000001f0230003900000011022001970000008002200039000000400020043f0000001f0430018f00000012053001980000008002500039000000240000613d0000008006000039000000000701034f000000007807043c0000000006860436000000000026004b000000200000c13d000000000004004b000000310000613d000000000151034f0000000304400210000000000502043300000000054501cf000000000545022f000000000101043b0000010004400089000000000141022f00000000014101cf000000000151019f0000000000120435000000200030008c000000350000813d00000000010000190000003e00010430000000800100043d000000000010041b00000020010000390000010000100443000001200000044300000013010000410000003d0001042e0000003c000004320000003d0001042e0000003e00010430000000000000000000000000000000000000000000000000000000000000000000000000ffffffff00000000000000000000000000000000000000000000000000000001ffffffe000000000000000000000000000000000000000000000000000000000ffffffe00000000200000000000000000000000000000040000001000000000000000000ffffffff000000000000000000000000000000000000000000000000000000008381f58a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000800000000000000000";

let output = cmd
.cast_fuse()
.args([
"estimate",
"--rpc-url",
&url,
"--from",
addr,
"--zksync",
"--create",
bytecode_with_constructor,
"constructor(uint256)",
"1",
])
.assert_success()
.get_output()
.stdout_lossy();
let output: u32 = output.trim().parse().unwrap();
assert!(output > 0);
});

0 comments on commit 5eaf1a7

Please sign in to comment.