Skip to content
Merged
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
21 changes: 2 additions & 19 deletions crates/cli/src/commands/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,

Expand Down Expand Up @@ -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<U256, String> {
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::*;
Expand Down
11 changes: 4 additions & 7 deletions crates/cli/src/default_scenarios/erc20.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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,

Expand All @@ -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,

Expand Down
11 changes: 6 additions & 5 deletions crates/cli/src/default_scenarios/setcode/execute.rs
Original file line number Diff line number Diff line change
@@ -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)]";
Expand Down Expand Up @@ -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<U256>,
}
Expand Down
8 changes: 4 additions & 4 deletions crates/cli/src/default_scenarios/transfers.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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,
Expand All @@ -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,
}
}
Expand Down
19 changes: 8 additions & 11 deletions crates/cli/src/default_scenarios/uni_v2.rs
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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,
Expand All @@ -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"
)]
Expand All @@ -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"]
)]
Expand All @@ -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"]
)]
Expand All @@ -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,
}
Expand Down
5 changes: 4 additions & 1 deletion crates/core/src/generator/function_def.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
pub value: Option<String>,
pub value: Option<String>, // may be a placeholder, so we can't use U256
pub fuzz: Vec<FuzzParam>,
pub kind: Option<String>,
pub gas_limit: Option<u64>,
Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/generator/templater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<U256>().ok());

Ok(TransactionRequest {
Expand Down
29 changes: 27 additions & 2 deletions crates/core/src/generator/trait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
Expand Down
33 changes: 32 additions & 1 deletion crates/core/src/generator/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};
Expand All @@ -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,
Expand Down Expand Up @@ -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<U256, UtilError> {
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};
Expand Down
2 changes: 1 addition & 1 deletion scenarios/bundles.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ gas_limit = 70000
to = "{SpamMe}"
from_pool = "bluepool"
signature = "tipCoinbase()"
value = "100000000000000"
value = "0.0001 eth"
gas_limit = 42000
2 changes: 1 addition & 1 deletion scenarios/slotContention.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ to = "{slotContention}"
kind = "fund_contract"
signature = "fundMe()"
args = []
value = "1000000000000000000"
value = "1eth"

# more likely to fail over time
[[spam]]
Expand Down