diff --git a/crates/cli/src/commands/common.rs b/crates/cli/src/commands/common.rs index 1dcfe788..7aeebad4 100644 --- a/crates/cli/src/commands/common.rs +++ b/crates/cli/src/commands/common.rs @@ -6,10 +6,10 @@ use crate::commands::SpamScenario; use crate::error::CliError; use crate::util::get_signers_with_defaults; use alloy::consensus::TxType; -use alloy::primitives::utils::parse_units; use alloy::primitives::U256; use alloy::providers::{DynProvider, ProviderBuilder}; use alloy::signers::local::PrivateKeySigner; +use contender_core::generator::util::parse_value; use contender_core::test_scenario::Url; use contender_core::BundleType; use contender_engine_provider::reth_node_api::EngineApiMessageVersion; @@ -69,7 +69,7 @@ Flag may be specified multiple times." long, long_help = "The minimum balance to keep in each spammer EOA, with units.", default_value = "0.01 ether", - value_parser = parse_amount, + value_parser = parse_value, )] pub min_balance: U256, @@ -443,23 +443,6 @@ pub fn cli_env_vars_parser(s: &str) -> Result<(String, String), String> { )) } -/// Parses an amount string with units (e.g., "1 ether", "100 gwei") into a U256 value. -/// Used for inline parsing of amounts in CLI arguments. -pub fn parse_amount(input: &str) -> Result { - let input = input.trim().to_lowercase(); - let (num_str, unit) = input.trim().split_at( - input - .find(|c: char| !c.is_numeric() && c != '.') - .ok_or("Missing unit in amount")?, - ); - let unit = unit.trim(); - let value: U256 = parse_units(num_str, unit) - .map_err(|e| format!("Failed to parse units: {e}"))? - .into(); - - Ok(value) -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/cli/src/default_scenarios/erc20.rs b/crates/cli/src/default_scenarios/erc20.rs index 5e1caf37..c9a3d08b 100644 --- a/crates/cli/src/default_scenarios/erc20.rs +++ b/crates/cli/src/default_scenarios/erc20.rs @@ -1,14 +1,11 @@ use alloy::primitives::{Address, U256}; use contender_core::generator::{ - types::SpamRequest, CreateDefinition, FunctionCallDefinition, FuzzParam, + types::SpamRequest, util::parse_value, CreateDefinition, FunctionCallDefinition, FuzzParam, }; use contender_testfile::TestConfig; use std::str::FromStr; -use crate::{ - commands::common::parse_amount, - default_scenarios::{builtin::ToTestConfig, contracts::test_token}, -}; +use crate::default_scenarios::{builtin::ToTestConfig, contracts::test_token}; #[derive(Clone, Default, Debug, clap::Parser)] pub struct Erc20CliArgs { @@ -17,7 +14,7 @@ pub struct Erc20CliArgs { long, long_help = "The amount to send in each spam tx.", default_value = "0.00001 ether", - value_parser = parse_amount, + value_parser = parse_value, )] pub send_amount: U256, @@ -26,7 +23,7 @@ pub struct Erc20CliArgs { long, long_help = "The amount of tokens to give each spammer account before spamming starts.", default_value = "1000000 ether", - value_parser = parse_amount, + value_parser = parse_value, )] pub fund_amount: U256, diff --git a/crates/cli/src/default_scenarios/setcode/execute.rs b/crates/cli/src/default_scenarios/setcode/execute.rs index 6a65cfe3..8c20d1e2 100644 --- a/crates/cli/src/default_scenarios/setcode/execute.rs +++ b/crates/cli/src/default_scenarios/setcode/execute.rs @@ -1,9 +1,10 @@ -use crate::{ - commands::common::parse_amount, default_scenarios::setcode::SetCodeCliArgs, error::CliError, -}; +use crate::{default_scenarios::setcode::SetCodeCliArgs, error::CliError}; use alloy::{hex::ToHexExt, primitives::U256}; use clap::Parser; -use contender_core::generator::{error::GeneratorError, util::encode_calldata}; +use contender_core::generator::{ + error::GeneratorError, + util::{encode_calldata, parse_value}, +}; pub const DEFAULT_SIG: &str = "execute((address,uint256,bytes)[])"; pub const DEFAULT_ARGS: &str = "[(0x{Counter},0,0xd09de08a)]"; @@ -48,7 +49,7 @@ Examples: Note: you must manually fund your setCode signer's account to use this feature. Use `contender admin setcode-signer` to get this account's details. Example: --value \"0.01 eth\"", - value_parser = parse_amount + value_parser = parse_value )] pub value: Option, } diff --git a/crates/cli/src/default_scenarios/transfers.rs b/crates/cli/src/default_scenarios/transfers.rs index e86de6a5..8dd5b3e9 100644 --- a/crates/cli/src/default_scenarios/transfers.rs +++ b/crates/cli/src/default_scenarios/transfers.rs @@ -1,7 +1,7 @@ -use crate::{commands::common::parse_amount, default_scenarios::builtin::ToTestConfig}; +use crate::default_scenarios::builtin::ToTestConfig; use alloy::primitives::{Address, U256}; use clap::Parser; -use contender_core::generator::{types::SpamRequest, FunctionCallDefinition}; +use contender_core::generator::{types::SpamRequest, util::parse_value, FunctionCallDefinition}; #[derive(Parser, Clone, Debug)] pub struct TransferStressCliArgs { @@ -10,7 +10,7 @@ pub struct TransferStressCliArgs { long = "transfer.amount", visible_aliases = ["ta", "amount"], default_value = "0.001 eth", - value_parser = parse_amount, + value_parser = parse_value, help = "Amount of tokens to transfer in each transaction." )] pub amount: U256, @@ -27,7 +27,7 @@ pub struct TransferStressCliArgs { impl Default for TransferStressCliArgs { fn default() -> Self { Self { - amount: parse_amount("0.001 eth").expect("valid default amount"), + amount: parse_value("0.001 eth").expect("valid default amount"), recipient: None, } } diff --git a/crates/cli/src/default_scenarios/uni_v2.rs b/crates/cli/src/default_scenarios/uni_v2.rs index 70d97a8d..2dc3c1ef 100644 --- a/crates/cli/src/default_scenarios/uni_v2.rs +++ b/crates/cli/src/default_scenarios/uni_v2.rs @@ -1,18 +1,15 @@ use std::str::FromStr; +use crate::default_scenarios::{builtin::ToTestConfig, contracts::test_token}; use alloy::primitives::U256; use clap::Parser; +use contender_core::generator::util::parse_value; use contender_core::generator::{ types::SpamRequest, CompiledContract, CreateDefinition, FunctionCallDefinition, }; use contender_testfile::TestConfig; use thiserror::Error; -use crate::{ - commands::common::parse_amount, - default_scenarios::{builtin::ToTestConfig, contracts::test_token}, -}; - #[derive(Debug, Clone, Parser)] pub struct UniV2CliArgs { #[arg( @@ -31,7 +28,7 @@ pub struct UniV2CliArgs { long_help = "The amount of ETH to deposit into each TOKEN pool. One additional multiple of this is also minted for trading.", default_value = "1 eth", value_name = "ETH_AMOUNT", - value_parser = parse_amount, + value_parser = parse_value, visible_aliases = ["weth"] )] pub weth_per_token: U256, @@ -41,7 +38,7 @@ pub struct UniV2CliArgs { long, long_help = "The initial amount minted for each token. 50% of this will be deposited among trading pools. Units must be provided, e.g. '1 eth' to mint 1 token with 1e18 decimal precision.", default_value = "5000000 eth", - value_parser = parse_amount, + value_parser = parse_value, visible_aliases = ["mint"], value_name = "TOKEN_AMOUNT" )] @@ -50,7 +47,7 @@ pub struct UniV2CliArgs { #[arg( long, long_help = "The amount of WETH to trade in the scenario. If not provided, 0.01% of the pool's initial WETH will be traded for each token. Units must be provided, e.g. '0.1 eth'.", - value_parser = parse_amount, + value_parser = parse_value, value_name = "WETH_AMOUNT", visible_aliases = ["trade-weth"] )] @@ -59,7 +56,7 @@ pub struct UniV2CliArgs { #[arg( long, long_help = "The amount of tokens to trade in the scenario. If not provided, 0.01% of the initial supply will be traded for each token.", - value_parser = parse_amount, + value_parser = parse_value, value_name = "TOKEN_AMOUNT", visible_aliases = ["trade-token"] )] @@ -70,8 +67,8 @@ impl Default for UniV2CliArgs { fn default() -> Self { Self { num_tokens: 2, - weth_per_token: parse_amount("1 eth").expect("valid default amount"), - initial_token_supply: parse_amount("5000000 eth").expect("valid default amount"), + weth_per_token: parse_value("1 eth").expect("valid default amount"), + initial_token_supply: parse_value("5000000 eth").expect("valid default amount"), weth_trade_amount: None, token_trade_amount: None, } diff --git a/crates/core/src/generator/function_def.rs b/crates/core/src/generator/function_def.rs index abf21fa1..003228f0 100644 --- a/crates/core/src/generator/function_def.rs +++ b/crates/core/src/generator/function_def.rs @@ -123,12 +123,15 @@ impl FunctionCallDefinition { } } +/// `FunctionCallDefinition` with a definite `from` address. +/// String fields may represent placeholders, which are replaced by +/// functions like `template_function_call`. pub struct FunctionCallDefinitionStrict { pub to: String, // may be a placeholder, so we can't use Address pub from: Address, pub signature: String, pub args: Vec, - pub value: Option, + pub value: Option, // may be a placeholder, so we can't use U256 pub fuzz: Vec, pub kind: Option, pub gas_limit: Option, diff --git a/crates/core/src/generator/templater.rs b/crates/core/src/generator/templater.rs index c15ec3e1..adba5956 100644 --- a/crates/core/src/generator/templater.rs +++ b/crates/core/src/generator/templater.rs @@ -176,7 +176,7 @@ where let value = funcdef .value .as_ref() - .map(|s| self.replace_placeholders(s, placeholder_map)) + .map(|x| self.replace_placeholders(x, placeholder_map)) .and_then(|s| s.parse::().ok()); Ok(TransactionRequest { diff --git a/crates/core/src/generator/trait.rs b/crates/core/src/generator/trait.rs index 34f213d7..1ee2e2cd 100644 --- a/crates/core/src/generator/trait.rs +++ b/crates/core/src/generator/trait.rs @@ -9,7 +9,7 @@ use crate::{ seeder::{SeedValue, Seeder}, templater::Templater, types::{AnyProvider, AsyncCallbackResult, PlanType, SpamRequest}, - util::UtilError, + util::{parse_value, UtilError}, CreateDefinition, CreateDefinitionStrict, RandSeed, }, spammer::CallbackError, @@ -289,12 +289,37 @@ where None }; + // if string is `(value, units)`, parse into U256 + // otherwise it may be a {placeholder}, so leave as-is + let value = if let Some(input) = funcdef.value.to_owned() { + Some(match parse_value(&input) { + Ok(u256) => Ok(u256.to_string()), + Err(e) => { + if self.get_templater().terminator_start(&input).is_none() { + // no placeholder detected + if input.chars().all(|c| c.is_numeric()) { + // treat as wei + Ok(input) + } else { + // error: not a placeholder; invalid "val+unit" string + Err(e) + } + } else { + // placeholder, leave string as-is + Ok(input) + } + } + }?) + } else { + None + }; + Ok(FunctionCallDefinitionStrict { to: to_address, from: from_address, signature: funcdef.signature.to_owned().unwrap_or_default(), args, - value: funcdef.value.to_owned(), + value, fuzz: funcdef.fuzz.to_owned().unwrap_or_default(), kind: funcdef.kind.to_owned(), gas_limit: funcdef.gas_limit.to_owned(), diff --git a/crates/core/src/generator/util.rs b/crates/core/src/generator/util.rs index 0f7dc25e..22b489e8 100644 --- a/crates/core/src/generator/util.rs +++ b/crates/core/src/generator/util.rs @@ -3,7 +3,10 @@ use alloy::{ consensus::TxType, dyn_abi::{self, DynSolType, DynSolValue, JsonAbiExt}, json_abi, - primitives::FixedBytes, + primitives::{ + utils::{parse_units, UnitsError}, + FixedBytes, U256, + }, rpc::types::TransactionRequest, signers::{self, local::PrivateKeySigner}, }; @@ -18,6 +21,12 @@ pub enum UtilError { #[error("failed to parse function signature: {0}")] ParseFunctionSigFailed(json_abi::parser::Error), + #[error("failed to parse 'value': {0}")] + ParseValueFailed(#[from] UnitsError), + + #[error("invalid `value` for tx: {0}. Must be wei (1234000000000000000) or contain units (1.234 eth)")] + InvalidValueNumeric(String), + #[error("invalid args for function signature '{sig}': {required} param(s) in sig, {given} args provided")] FunctionSignatureNumArgsInvalid { sig: String, @@ -164,6 +173,28 @@ pub fn generate_setcode_signer(seed: &impl Seeder) -> (PrivateKeySigner, [u8; 32 ) } +/// Parses a string like "1eth" or "20 gwei" into a U256. +pub fn parse_value(input: &str) -> Result { + let input = input.trim().to_lowercase(); + + match input.parse() { + Ok(value) => Ok(value), + Err(_) => { + let (num_str, unit) = input.trim().split_at( + input + .find(|c: char| !c.is_numeric() && c != '.') + .ok_or(UtilError::InvalidValueNumeric(input.clone()))?, + ); + let unit = unit.trim(); + let value: U256 = parse_units(num_str, unit) + .map_err(UtilError::ParseValueFailed)? + .into(); + + Ok(value) + } + } +} + #[cfg(test)] pub mod test { use alloy::node_bindings::{Anvil, AnvilInstance}; diff --git a/scenarios/bundles.toml b/scenarios/bundles.toml index 303b092b..74aa9c4d 100644 --- a/scenarios/bundles.toml +++ b/scenarios/bundles.toml @@ -17,5 +17,5 @@ gas_limit = 70000 to = "{SpamMe}" from_pool = "bluepool" signature = "tipCoinbase()" -value = "100000000000000" +value = "0.0001 eth" gas_limit = 42000 diff --git a/scenarios/slotContention.toml b/scenarios/slotContention.toml index dbad52ff..d2d5fda9 100644 --- a/scenarios/slotContention.toml +++ b/scenarios/slotContention.toml @@ -9,7 +9,7 @@ to = "{slotContention}" kind = "fund_contract" signature = "fundMe()" args = [] -value = "1000000000000000000" +value = "1eth" # more likely to fail over time [[spam]]