From 56d8b0df6bba1e56aa7fbfc26b790d9f2cac1262 Mon Sep 17 00:00:00 2001 From: parizval Date: Fri, 5 Dec 2025 00:18:09 +0530 Subject: [PATCH 1/6] refactor: updated data type of Value of FunctionCallDefinition to U256 --- crates/core/src/generator/function_def.rs | 6 +++--- crates/core/src/generator/templater.rs | 2 +- crates/core/src/generator/trait.rs | 3 ++- crates/testfile/src/lib.rs | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/core/src/generator/function_def.rs b/crates/core/src/generator/function_def.rs index abf21fa1..4845766c 100644 --- a/crates/core/src/generator/function_def.rs +++ b/crates/core/src/generator/function_def.rs @@ -21,7 +21,7 @@ pub struct FunctionCallDefinition { /// Parameters to pass to the function. pub args: Option>, /// Value in wei to send with the tx. - pub value: Option, + pub value: Option, /// Parameters to fuzz during the test. pub fuzz: Option>, /// Optional type of the spam transaction for categorization. @@ -80,7 +80,7 @@ impl FunctionCallDefinition { } /// Set value in wei to send with the tx. pub fn with_value(mut self, value: U256) -> Self { - self.value = Some(value.to_string()); + self.value = Some(value); self } pub fn with_fuzz(mut self, fuzz: &[FuzzParam]) -> Self { @@ -128,7 +128,7 @@ pub struct FunctionCallDefinitionStrict { pub from: Address, pub signature: String, pub args: Vec, - pub value: Option, + pub value: Option, 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..40b1b31d 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(|s| self.replace_placeholders(&s.to_string(), 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 d77cd306..b1e2ad3d 100644 --- a/crates/core/src/generator/trait.rs +++ b/crates/core/src/generator/trait.rs @@ -471,7 +471,8 @@ where req.args = Some(args); if fuzz_tx_value.is_some() { - req.value = fuzz_tx_value; + req.value = + Some(fuzz_tx_value.unwrap().parse().unwrap_or_default()); } let tx = NamedTxRequest::new( diff --git a/crates/testfile/src/lib.rs b/crates/testfile/src/lib.rs index 66bf10be..c0840a77 100644 --- a/crates/testfile/src/lib.rs +++ b/crates/testfile/src/lib.rs @@ -191,7 +191,7 @@ pub mod tests { "0x70997970C51812dc3A010C7d01b50e0d17dc79C8".to_owned() ); assert_eq!(setup.len(), 1); - assert_eq!(setup[0].value, Some("1234".to_owned())); + assert_eq!(setup[0].value, Some(U256::from(1234))); assert_eq!( fncall.fuzz.as_ref().unwrap()[0].param.to_owned().unwrap(), "amountIn" @@ -467,7 +467,7 @@ mySender = \"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266\" [spam.tx] from = \"{mySender}\" to = \"{_sender}\" -value = \"1eth\" +value = \"1000000000000000000\" ", ) .unwrap(); From f0a99ddc1db46102cb07474c325388b3985ae22b Mon Sep 17 00:00:00 2001 From: parizval Date: Fri, 5 Dec 2025 00:28:20 +0530 Subject: [PATCH 2/6] refactor: improved unwrapping logic in load_txs function --- crates/core/src/generator/trait.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/core/src/generator/trait.rs b/crates/core/src/generator/trait.rs index b1e2ad3d..cd332e13 100644 --- a/crates/core/src/generator/trait.rs +++ b/crates/core/src/generator/trait.rs @@ -470,9 +470,9 @@ where let mut req = req.to_owned(); req.args = Some(args); - if fuzz_tx_value.is_some() { + if let Some(fuzz_tx_value_unwrapped) = fuzz_tx_value { req.value = - Some(fuzz_tx_value.unwrap().parse().unwrap_or_default()); + Some(fuzz_tx_value_unwrapped.parse().unwrap_or_default()); } let tx = NamedTxRequest::new( From 22dbdff7204aa1c81ffb5844acb2b4f9e3976c61 Mon Sep 17 00:00:00 2001 From: parizval Date: Tue, 9 Dec 2025 22:05:19 +0530 Subject: [PATCH 3/6] refactor: updated data type of Value of FunctionCallDefinitionStrict to U256 and parse_value --- crates/core/src/generator/error.rs | 3 ++ crates/core/src/generator/function_def.rs | 4 +-- crates/core/src/generator/trait.rs | 35 ++++++++++++++++++++--- crates/testfile/src/lib.rs | 4 +-- 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/crates/core/src/generator/error.rs b/crates/core/src/generator/error.rs index 3bb57363..4c5a049b 100644 --- a/crates/core/src/generator/error.rs +++ b/crates/core/src/generator/error.rs @@ -22,6 +22,9 @@ pub enum GeneratorError { error:
::Err, }, + #[error("failed to parse 'value': {0}")] + ValueParseFailed(String), + #[error("from_pool {0} not found in agent store")] FromPoolNotFound(String), diff --git a/crates/core/src/generator/function_def.rs b/crates/core/src/generator/function_def.rs index 4845766c..8ea26234 100644 --- a/crates/core/src/generator/function_def.rs +++ b/crates/core/src/generator/function_def.rs @@ -21,7 +21,7 @@ pub struct FunctionCallDefinition { /// Parameters to pass to the function. pub args: Option>, /// Value in wei to send with the tx. - pub value: Option, + pub value: Option, /// Parameters to fuzz during the test. pub fuzz: Option>, /// Optional type of the spam transaction for categorization. @@ -80,7 +80,7 @@ impl FunctionCallDefinition { } /// Set value in wei to send with the tx. pub fn with_value(mut self, value: U256) -> Self { - self.value = Some(value); + self.value = Some(value.to_string()); self } pub fn with_fuzz(mut self, fuzz: &[FuzzParam]) -> Self { diff --git a/crates/core/src/generator/trait.rs b/crates/core/src/generator/trait.rs index cd332e13..4bc82078 100644 --- a/crates/core/src/generator/trait.rs +++ b/crates/core/src/generator/trait.rs @@ -17,6 +17,7 @@ use crate::{ use alloy::{ eips::eip7702::SignedAuthorization, hex::ToHexExt, + primitives::utils::parse_units, primitives::{keccak256, Address, FixedBytes, U256}, rpc::types::Authorization, signers::{local::PrivateKeySigner, SignerSync}, @@ -99,6 +100,27 @@ where } } +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(GeneratorError::ValueParseFailed(input.to_string()))?, + ); + let unit = unit.trim(); + let value: U256 = parse_units(num_str, unit) + .map_err(|_| GeneratorError::ValueParseFailed(input.to_string()))? + .into(); + + Ok(value) + } + } +} + #[async_trait] pub trait Generator where @@ -288,12 +310,18 @@ where None }; + let value = if let Some(input) = funcdef.value.to_owned() { + Some(parse_value(&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(), @@ -470,9 +498,8 @@ where let mut req = req.to_owned(); req.args = Some(args); - if let Some(fuzz_tx_value_unwrapped) = fuzz_tx_value { - req.value = - Some(fuzz_tx_value_unwrapped.parse().unwrap_or_default()); + if fuzz_tx_value.is_some() { + req.value = fuzz_tx_value; } let tx = NamedTxRequest::new( diff --git a/crates/testfile/src/lib.rs b/crates/testfile/src/lib.rs index c0840a77..66bf10be 100644 --- a/crates/testfile/src/lib.rs +++ b/crates/testfile/src/lib.rs @@ -191,7 +191,7 @@ pub mod tests { "0x70997970C51812dc3A010C7d01b50e0d17dc79C8".to_owned() ); assert_eq!(setup.len(), 1); - assert_eq!(setup[0].value, Some(U256::from(1234))); + assert_eq!(setup[0].value, Some("1234".to_owned())); assert_eq!( fncall.fuzz.as_ref().unwrap()[0].param.to_owned().unwrap(), "amountIn" @@ -467,7 +467,7 @@ mySender = \"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266\" [spam.tx] from = \"{mySender}\" to = \"{_sender}\" -value = \"1000000000000000000\" +value = \"1eth\" ", ) .unwrap(); From 5f5a45e764b62e4f98a0b510b908442d21972bb1 Mon Sep 17 00:00:00 2001 From: parizval Date: Tue, 9 Dec 2025 22:11:03 +0530 Subject: [PATCH 4/6] refactor: updated value transformation in fn template_function_call --- crates/core/src/generator/templater.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/core/src/generator/templater.rs b/crates/core/src/generator/templater.rs index 40b1b31d..ac9ddb7b 100644 --- a/crates/core/src/generator/templater.rs +++ b/crates/core/src/generator/templater.rs @@ -9,7 +9,7 @@ use crate::{ }; use alloy::{ hex::{FromHex, ToHexExt}, - primitives::{Address, Bytes, FixedBytes, TxKind, U256}, + primitives::{Address, Bytes, FixedBytes, TxKind}, rpc::types::TransactionRequest, }; use std::collections::HashMap; @@ -173,11 +173,7 @@ where let to = to .parse::
() .map_err(|_| TemplaterError::ParseAddressFailed(to))?; - let value = funcdef - .value - .as_ref() - .map(|s| self.replace_placeholders(&s.to_string(), placeholder_map)) - .and_then(|s| s.parse::().ok()); + let value = funcdef.value; Ok(TransactionRequest { to: Some(TxKind::Call(to)), From c32e65aef6fb68bfce714074cce45381335c9080 Mon Sep 17 00:00:00 2001 From: zeroXbrock <2791467+zeroXbrock@users.noreply.github.com> Date: Wed, 17 Dec 2025 20:29:25 -0800 Subject: [PATCH 5/6] DRY parse_value/parse_amount, more specific parse errors, correct FunctionCallDefinitionStrict error (U256 -> String) --- crates/cli/src/commands/common.rs | 21 +-------- crates/cli/src/default_scenarios/erc20.rs | 11 ++--- .../src/default_scenarios/setcode/execute.rs | 11 ++--- crates/cli/src/default_scenarios/transfers.rs | 8 ++-- crates/cli/src/default_scenarios/uni_v2.rs | 19 ++++---- crates/core/src/generator/error.rs | 3 -- crates/core/src/generator/function_def.rs | 5 ++- crates/core/src/generator/templater.rs | 8 +++- crates/core/src/generator/trait.rs | 45 +++++++++---------- crates/core/src/generator/util.rs | 33 +++++++++++++- scenarios/bundles.toml | 2 +- scenarios/slotContention.toml | 2 +- 12 files changed, 89 insertions(+), 79 deletions(-) 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/error.rs b/crates/core/src/generator/error.rs index 4c5a049b..3bb57363 100644 --- a/crates/core/src/generator/error.rs +++ b/crates/core/src/generator/error.rs @@ -22,9 +22,6 @@ pub enum GeneratorError { error:
::Err, }, - #[error("failed to parse 'value': {0}")] - ValueParseFailed(String), - #[error("from_pool {0} not found in agent store")] FromPoolNotFound(String), diff --git a/crates/core/src/generator/function_def.rs b/crates/core/src/generator/function_def.rs index 8ea26234..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 ac9ddb7b..748c0b6b 100644 --- a/crates/core/src/generator/templater.rs +++ b/crates/core/src/generator/templater.rs @@ -9,7 +9,7 @@ use crate::{ }; use alloy::{ hex::{FromHex, ToHexExt}, - primitives::{Address, Bytes, FixedBytes, TxKind}, + primitives::{Address, Bytes, FixedBytes, TxKind, U256}, rpc::types::TransactionRequest, }; use std::collections::HashMap; @@ -173,7 +173,11 @@ where let to = to .parse::
() .map_err(|_| TemplaterError::ParseAddressFailed(to))?; - let value = funcdef.value; + let value = funcdef + .value + .as_ref() + .map(|x| self.replace_placeholders(x, &placeholder_map)) + .and_then(|s| s.parse::().ok()); Ok(TransactionRequest { to: Some(TxKind::Call(to)), diff --git a/crates/core/src/generator/trait.rs b/crates/core/src/generator/trait.rs index 86fbd990..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, @@ -17,7 +17,6 @@ use crate::{ use alloy::{ eips::eip7702::SignedAuthorization, hex::ToHexExt, - primitives::utils::parse_units, primitives::{keccak256, Address, FixedBytes, U256}, providers::Provider, rpc::types::Authorization, @@ -101,27 +100,6 @@ where } } -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(GeneratorError::ValueParseFailed(input.to_string()))?, - ); - let unit = unit.trim(); - let value: U256 = parse_units(num_str, unit) - .map_err(|_| GeneratorError::ValueParseFailed(input.to_string()))? - .into(); - - Ok(value) - } - } -} - #[async_trait] pub trait Generator where @@ -311,8 +289,27 @@ 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(parse_value(&input)?) + 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 }; diff --git a/crates/core/src/generator/util.rs b/crates/core/src/generator/util.rs index 0f7dc25e..8f2091a8 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(|e| UtilError::ParseValueFailed(e))? + .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]] From db7a02ba5802d56de15b1d36ccd9edf7fd585836 Mon Sep 17 00:00:00 2001 From: zeroXbrock <2791467+zeroXbrock@users.noreply.github.com> Date: Wed, 17 Dec 2025 20:36:17 -0800 Subject: [PATCH 6/6] chore: clippy --- crates/core/src/generator/templater.rs | 2 +- crates/core/src/generator/util.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/core/src/generator/templater.rs b/crates/core/src/generator/templater.rs index 748c0b6b..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(|x| self.replace_placeholders(x, &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/util.rs b/crates/core/src/generator/util.rs index 8f2091a8..22b489e8 100644 --- a/crates/core/src/generator/util.rs +++ b/crates/core/src/generator/util.rs @@ -187,7 +187,7 @@ pub fn parse_value(input: &str) -> Result { ); let unit = unit.trim(); let value: U256 = parse_units(num_str, unit) - .map_err(|e| UtilError::ParseValueFailed(e))? + .map_err(UtilError::ParseValueFailed)? .into(); Ok(value)