diff --git a/crates/common/src/macros/define_wrapper_type.rs b/crates/common/src/macros/define_wrapper_type.rs index 28a9e53..7eb28bc 100644 --- a/crates/common/src/macros/define_wrapper_type.rs +++ b/crates/common/src/macros/define_wrapper_type.rs @@ -48,11 +48,7 @@ macro_rules! define_wrapper_type { $vis struct $ident($ty); impl $ident { - pub fn new(value: $ty) -> Self { - Self(value) - } - - pub fn new_from>(value: T) -> Self { + pub fn new(value: impl Into<$ty>) -> Self { Self(value.into()) } diff --git a/crates/core/src/driver/mod.rs b/crates/core/src/driver/mod.rs index a78100b..5267c52 100644 --- a/crates/core/src/driver/mod.rs +++ b/crates/core/src/driver/mod.rs @@ -509,7 +509,7 @@ where .iter() .zip(actual_event.topics()) { - let expected = Calldata::Compound(vec![expected.clone()]); + let expected = Calldata::new_compound([expected]); if !expected.is_equivalent( &actual.0, deployed_contracts, @@ -718,7 +718,7 @@ where ); let _guard = tracing_span.enter(); - let case_idx = CaseIdx::new_from(case_idx); + let case_idx = CaseIdx::new(case_idx); // For inputs if one of the inputs fail we move on to the next case (we do not move // on to the next input as it doesn't make sense. It depends on the previous one). diff --git a/crates/format/src/input.rs b/crates/format/src/input.rs index 68d491d..78f4693 100644 --- a/crates/format/src/input.rs +++ b/crates/format/src/input.rs @@ -8,6 +8,7 @@ use alloy::{ rpc::types::TransactionRequest, }; use alloy_primitives::{FixedBytes, utils::parse_units}; +use anyhow::Context; use semver::VersionReq; use serde::{Deserialize, Serialize}; @@ -18,10 +19,10 @@ use crate::traits::ResolverApi; #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] pub struct Input { - #[serde(default = "default_caller")] + #[serde(default = "Input::default_caller")] pub caller: Address, pub comment: Option, - #[serde(default = "default_instance")] + #[serde(default = "Input::default_instance")] pub instance: ContractInstance, pub method: Method, #[serde(default)] @@ -55,11 +56,86 @@ pub struct Event { pub values: Calldata, } +/// A type definition for the calldata supported by the testing framework. +/// +/// We choose to document all of the types used in [`Calldata`] in this one doc comment to elaborate +/// on why they exist and consolidate all of the documentation for calldata in a single place where +/// it can be viewed and understood. +/// +/// The [`Single`] variant of this enum is quite simple and straightforward: it's a hex-encoded byte +/// array of the calldata. +/// +/// The [`Compound`] type is more intricate and allows for capabilities such as resolution and some +/// simple arithmetic operations. It houses a vector of [`CalldataItem`]s which is just a wrapper +/// around an owned string. +/// +/// A [`CalldataItem`] could be a simple hex string of a single calldata argument, but it could also +/// be something that requires resolution such as `MyContract.address` which is a variable that is +/// understood by the resolution logic to mean "Lookup the address of this particular contract +/// instance". +/// +/// In addition to the above, the format supports some simple arithmetic operations like add, sub, +/// divide, multiply, bitwise AND, bitwise OR, and bitwise XOR. Our parser understands the [reverse +/// polish notation] simply because it's easy to write a calculator for that notation and since we +/// do not have plans to use arithmetic too often in tests. In reverse polish notation a typical +/// `2 + 4` would be written as `2 4 +` which makes this notation very simple to implement through +/// a stack. +/// +/// Combining the above, a single [`CalldataItem`] could employ both resolution and arithmetic at +/// the same time. For example, a [`CalldataItem`] of `$BLOCK_NUMBER $BLOCK_NUMBER +` means that +/// the block number should be retrieved and then it should be added to itself. +/// +/// Internally, we split the [`CalldataItem`] by spaces. Therefore, `$BLOCK_NUMBER $BLOCK_NUMBER+` +/// is invalid but `$BLOCK_NUMBER $BLOCK_NUMBER +` is valid and can be understood by the parser and +/// calculator. After the split is done, each token is parsed into a [`CalldataToken<&str>`] forming +/// an [`Iterator`] over [`CalldataToken<&str>`]. A [`CalldataToken<&str>`] can then be resolved +/// into a [`CalldataToken`] through the resolution logic. Finally, after resolution is done, +/// this iterator of [`CalldataToken`] is collapsed into the final result by applying the +/// arithmetic operations requested. +/// +/// For example, supplying a [`Compound`] calldata of `0xdeadbeef` produces an iterator of a single +/// [`CalldataToken<&str>`] items of the value [`CalldataToken::Item`] of the string value 12 which +/// we can then resolve into the appropriate [`U256`] value and convert into calldata. +/// +/// In summary, the various types used in [`Calldata`] represent the following: +/// - [`CalldataItem`]: A calldata string from the metadata files. +/// - [`CalldataToken<&str>`]: Typically used in an iterator of items from the space splitted +/// [`CalldataItem`] and represents a token that has not yet been resolved into its value. +/// - [`CalldataToken`]: Represents a token that's been resolved from being a string and into +/// the word-size calldata argument on which we can perform arithmetic. +/// +/// [`Single`]: Calldata::Single +/// [`Compound`]: Calldata::Compound +/// [reverse polish notation]: https://en.wikipedia.org/wiki/Reverse_Polish_notation #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] #[serde(untagged)] pub enum Calldata { Single(Bytes), - Compound(Vec), + Compound(Vec), +} + +define_wrapper_type! { + /// This represents an item in the [`Calldata::Compound`] variant. + #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] + #[serde(transparent)] + pub struct CalldataItem(String); +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +enum CalldataToken { + Item(T), + Operation(Operation), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +enum Operation { + Addition, + Subtraction, + Multiplication, + Division, + BitwiseAnd, + BitwiseOr, + BitwiseXor, } /// Specify how the contract is called. @@ -88,154 +164,17 @@ define_wrapper_type!( pub struct EtherValue(U256); ); -impl Serialize for EtherValue { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - format!("{} wei", self.0).serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for EtherValue { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let string = String::deserialize(deserializer)?; - let mut splitted = string.split(' '); - let (Some(value), Some(unit)) = (splitted.next(), splitted.next()) else { - return Err(serde::de::Error::custom("Failed to parse the value")); - }; - let parsed = parse_units(value, unit.replace("eth", "ether")) - .map_err(|_| serde::de::Error::custom("Failed to parse units"))? - .into(); - Ok(Self(parsed)) - } -} - -impl ExpectedOutput { - pub fn new() -> Self { - Default::default() - } - - pub fn with_success(mut self) -> Self { - self.exception = false; - self - } - - pub fn with_failure(mut self) -> Self { - self.exception = true; - self - } - - pub fn with_calldata(mut self, calldata: Calldata) -> Self { - self.return_data = Some(calldata); - self - } -} - -impl Default for Calldata { - fn default() -> Self { - Self::Compound(Default::default()) - } -} - -impl Calldata { - pub fn find_all_contract_instances(&self, vec: &mut Vec) { - if let Calldata::Compound(compound) = self { - for item in compound { - if let Some(instance) = item.strip_suffix(".address") { - vec.push(ContractInstance::new_from(instance)) - } - } - } - } - - pub fn calldata( - &self, - deployed_contracts: &HashMap, - chain_state_provider: &impl ResolverApi, - ) -> anyhow::Result> { - let mut buffer = Vec::::with_capacity(self.size_requirement()); - self.calldata_into_slice(&mut buffer, deployed_contracts, chain_state_provider)?; - Ok(buffer) - } - - pub fn calldata_into_slice( - &self, - buffer: &mut Vec, - deployed_contracts: &HashMap, - chain_state_provider: &impl ResolverApi, - ) -> anyhow::Result<()> { - match self { - Calldata::Single(bytes) => { - buffer.extend_from_slice(bytes); - } - Calldata::Compound(items) => { - for (arg_idx, arg) in items.iter().enumerate() { - match resolve_argument(arg, deployed_contracts, chain_state_provider) { - Ok(resolved) => { - buffer.extend(resolved.to_be_bytes::<32>()); - } - Err(error) => { - tracing::error!(arg, arg_idx, ?error, "Failed to resolve argument"); - return Err(error); - } - }; - } - } - }; - Ok(()) - } - - pub fn size_requirement(&self) -> usize { - match self { - Calldata::Single(single) => single.len(), - Calldata::Compound(items) => items.len() * 32, - } - } - - /// Checks if this [`Calldata`] is equivalent to the passed calldata bytes. - pub fn is_equivalent( - &self, - other: &[u8], - deployed_contracts: &HashMap, - chain_state_provider: &impl ResolverApi, - ) -> anyhow::Result { - match self { - Calldata::Single(calldata) => Ok(calldata == other), - Calldata::Compound(items) => { - // Chunking the "other" calldata into 32 byte chunks since each - // one of the items in the compound calldata represents 32 bytes - for (this, other) in items.iter().zip(other.chunks(32)) { - // The matterlabs format supports wildcards and therefore we - // also need to support them. - if this == "*" { - continue; - } - - let other = if other.len() < 32 { - let mut vec = other.to_vec(); - vec.resize(32, 0); - std::borrow::Cow::Owned(vec) - } else { - std::borrow::Cow::Borrowed(other) - }; - - let this = resolve_argument(this, deployed_contracts, chain_state_provider)?; - let other = U256::from_be_slice(&other); - if this != other { - return Ok(false); - } - } - Ok(true) - } - } - } -} - impl Input { + pub const fn default_caller() -> Address { + Address(FixedBytes(alloy::hex!( + "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1" + ))) + } + + fn default_instance() -> ContractInstance { + ContractInstance::new("Test") + } + fn instance_to_address( &self, instance: &ContractInstance, @@ -343,84 +282,337 @@ impl Input { } } -fn default_instance() -> ContractInstance { - ContractInstance::new_from("Test") +impl ExpectedOutput { + pub fn new() -> Self { + Default::default() + } + + pub fn with_success(mut self) -> Self { + self.exception = false; + self + } + + pub fn with_failure(mut self) -> Self { + self.exception = true; + self + } + + pub fn with_calldata(mut self, calldata: Calldata) -> Self { + self.return_data = Some(calldata); + self + } } -pub const fn default_caller() -> Address { - Address(FixedBytes(alloy::hex!( - "90F8bf6A479f320ead074411a4B0e7944Ea8c9C1" - ))) +impl Default for Calldata { + fn default() -> Self { + Self::Compound(Default::default()) + } } -/// This function takes in the string calldata argument provided in the JSON input and resolves it -/// into a [`U256`] which is later used to construct the calldata. -/// -/// # Note -/// -/// This piece of code is taken from the matter-labs-tester repository which is licensed under MIT -/// or Apache. The original source code can be found here: -/// https://github.com/matter-labs/era-compiler-tester/blob/0ed598a27f6eceee7008deab3ff2311075a2ec69/compiler_tester/src/test/case/input/value.rs#L43-L146 -fn resolve_argument( - value: &str, - deployed_contracts: &HashMap, - chain_state_provider: &impl ResolverApi, -) -> anyhow::Result { - if let Some(instance) = value.strip_suffix(".address") { - Ok(U256::from_be_slice( - deployed_contracts - .get(&ContractInstance::new_from(instance)) - .map(|(a, _)| *a) - .ok_or_else(|| anyhow::anyhow!("Instance `{}` not found", instance))? - .as_ref(), - )) - } else if let Some(value) = value.strip_prefix('-') { - let value = U256::from_str_radix(value, 10) - .map_err(|error| anyhow::anyhow!("Invalid decimal literal after `-`: {}", error))?; - if value > U256::ONE << 255u8 { - anyhow::bail!("Decimal literal after `-` is too big"); +impl Calldata { + pub fn new_single(item: impl Into) -> Self { + Self::Single(item.into()) + } + + pub fn new_compound(items: impl IntoIterator>) -> Self { + Self::Compound( + items + .into_iter() + .map(|item| item.as_ref().to_owned()) + .map(CalldataItem::new) + .collect(), + ) + } + + pub fn find_all_contract_instances(&self, vec: &mut Vec) { + if let Calldata::Compound(compound) = self { + for item in compound { + if let Some(instance) = + item.strip_suffix(CalldataToken::<()>::ADDRESS_VARIABLE_SUFFIX) + { + vec.push(ContractInstance::new(instance)) + } + } } - let value = value - .checked_sub(U256::ONE) - .ok_or_else(|| anyhow::anyhow!("`-0` is invalid literal"))?; - Ok(U256::MAX.checked_sub(value).expect("Always valid")) - } else if let Some(value) = value.strip_prefix("0x") { - Ok(U256::from_str_radix(value, 16) - .map_err(|error| anyhow::anyhow!("Invalid hexadecimal literal: {}", error))?) - } else if value == "$CHAIN_ID" { - let chain_id = chain_state_provider.chain_id()?; - Ok(U256::from(chain_id)) - } else if value == "$GAS_LIMIT" { - let gas_limit = chain_state_provider.block_gas_limit(BlockNumberOrTag::Latest)?; - Ok(U256::from(gas_limit)) - } else if value == "$COINBASE" { - let coinbase = chain_state_provider.block_coinbase(BlockNumberOrTag::Latest)?; - Ok(U256::from_be_slice(coinbase.as_ref())) - } else if value == "$DIFFICULTY" { - let block_difficulty = chain_state_provider.block_difficulty(BlockNumberOrTag::Latest)?; - Ok(block_difficulty) - } else if value.starts_with("$BLOCK_HASH") { - let offset: u64 = value - .split(':') - .next_back() - .and_then(|value| value.parse().ok()) - .unwrap_or_default(); + } - let current_block_number = chain_state_provider.last_block_number()?; - let desired_block_number = current_block_number - offset; + pub fn calldata( + &self, + deployed_contracts: &HashMap, + chain_state_provider: &impl ResolverApi, + ) -> anyhow::Result> { + let mut buffer = Vec::::with_capacity(self.size_requirement()); + self.calldata_into_slice(&mut buffer, deployed_contracts, chain_state_provider)?; + Ok(buffer) + } - let block_hash = chain_state_provider.block_hash(desired_block_number.into())?; + pub fn calldata_into_slice( + &self, + buffer: &mut Vec, + deployed_contracts: &HashMap, + chain_state_provider: &impl ResolverApi, + ) -> anyhow::Result<()> { + match self { + Calldata::Single(bytes) => { + buffer.extend_from_slice(bytes); + } + Calldata::Compound(items) => { + for (arg_idx, arg) in items.iter().enumerate() { + match arg.resolve(deployed_contracts, chain_state_provider) { + Ok(resolved) => { + buffer.extend(resolved.to_be_bytes::<32>()); + } + Err(error) => { + tracing::error!(?arg, arg_idx, ?error, "Failed to resolve argument"); + return Err(error); + } + }; + } + } + }; + Ok(()) + } - Ok(U256::from_be_bytes(block_hash.0)) - } else if value == "$BLOCK_NUMBER" { - let current_block_number = chain_state_provider.last_block_number()?; - Ok(U256::from(current_block_number)) - } else if value == "$BLOCK_TIMESTAMP" { - let timestamp = chain_state_provider.block_timestamp(BlockNumberOrTag::Latest)?; - Ok(U256::from(timestamp)) - } else { - Ok(U256::from_str_radix(value, 10) - .map_err(|error| anyhow::anyhow!("Invalid decimal literal: {}", error))?) + pub fn size_requirement(&self) -> usize { + match self { + Calldata::Single(single) => single.len(), + Calldata::Compound(items) => items.len() * 32, + } + } + + /// Checks if this [`Calldata`] is equivalent to the passed calldata bytes. + pub fn is_equivalent( + &self, + other: &[u8], + deployed_contracts: &HashMap, + chain_state_provider: &impl ResolverApi, + ) -> anyhow::Result { + match self { + Calldata::Single(calldata) => Ok(calldata == other), + Calldata::Compound(items) => { + // Chunking the "other" calldata into 32 byte chunks since each + // one of the items in the compound calldata represents 32 bytes + for (this, other) in items.iter().zip(other.chunks(32)) { + // The matterlabs format supports wildcards and therefore we + // also need to support them. + if this.as_ref() == "*" { + continue; + } + + let other = if other.len() < 32 { + let mut vec = other.to_vec(); + vec.resize(32, 0); + std::borrow::Cow::Owned(vec) + } else { + std::borrow::Cow::Borrowed(other) + }; + + let this = this.resolve(deployed_contracts, chain_state_provider)?; + let other = U256::from_be_slice(&other); + if this != other { + return Ok(false); + } + } + Ok(true) + } + } + } +} + +impl CalldataItem { + fn resolve( + &self, + deployed_contracts: &HashMap, + chain_state_provider: &impl ResolverApi, + ) -> anyhow::Result { + let mut stack = Vec::>::new(); + + for token in self + .calldata_tokens() + .map(|token| token.resolve(deployed_contracts, chain_state_provider)) + { + let token = token?; + let new_token = match token { + CalldataToken::Item(_) => token, + CalldataToken::Operation(operation) => { + let right_operand = stack + .pop() + .and_then(CalldataToken::into_item) + .context("Invalid calldata arithmetic operation")?; + let left_operand = stack + .pop() + .and_then(CalldataToken::into_item) + .context("Invalid calldata arithmetic operation")?; + + let result = match operation { + Operation::Addition => left_operand.checked_add(right_operand), + Operation::Subtraction => left_operand.checked_sub(right_operand), + Operation::Multiplication => left_operand.checked_mul(right_operand), + Operation::Division => left_operand.checked_div(right_operand), + Operation::BitwiseAnd => Some(left_operand & right_operand), + Operation::BitwiseOr => Some(left_operand | right_operand), + Operation::BitwiseXor => Some(left_operand ^ right_operand), + } + .context("Invalid calldata arithmetic operation")?; + + CalldataToken::Item(result) + } + }; + stack.push(new_token) + } + + match stack.as_slice() { + // Empty stack means that we got an empty compound calldata which we resolve to zero. + [] => Ok(U256::ZERO), + [CalldataToken::Item(item)] => Ok(*item), + _ => Err(anyhow::anyhow!("Invalid calldata arithmetic operation")), + } + } + + fn calldata_tokens<'a>(&'a self) -> impl Iterator> + 'a { + self.0.split(' ').map(|item| match item { + "+" => CalldataToken::Operation(Operation::Addition), + "-" => CalldataToken::Operation(Operation::Subtraction), + "/" => CalldataToken::Operation(Operation::Division), + "*" => CalldataToken::Operation(Operation::Multiplication), + "&" => CalldataToken::Operation(Operation::BitwiseAnd), + "|" => CalldataToken::Operation(Operation::BitwiseOr), + "^" => CalldataToken::Operation(Operation::BitwiseXor), + _ => CalldataToken::Item(item), + }) + } +} + +impl CalldataToken { + const ADDRESS_VARIABLE_SUFFIX: &str = ".address"; + const NEGATIVE_VALUE_PREFIX: char = '-'; + const HEX_LITERAL_PREFIX: &str = "0x"; + const CHAIN_VARIABLE: &str = "$CHAIN_ID"; + const GAS_LIMIT_VARIABLE: &str = "$GAS_LIMIT"; + const COINBASE_VARIABLE: &str = "$COINBASE"; + const DIFFICULTY_VARIABLE: &str = "$DIFFICULTY"; + const BLOCK_HASH_VARIABLE_PREFIX: &str = "$BLOCK_HASH"; + const BLOCK_NUMBER_VARIABLE: &str = "$BLOCK_NUMBER"; + const BLOCK_TIMESTAMP_VARIABLE: &str = "$BLOCK_TIMESTAMP"; + + fn into_item(self) -> Option { + match self { + CalldataToken::Item(item) => Some(item), + CalldataToken::Operation(_) => None, + } + } +} + +impl> CalldataToken { + /// This function takes in the string calldata argument provided in the JSON input and resolves + /// it into a [`U256`] which is later used to construct the calldata. + /// + /// # Note + /// + /// This piece of code is taken from the matter-labs-tester repository which is licensed under + /// MIT or Apache. The original source code can be found here: + /// https://github.com/matter-labs/era-compiler-tester/blob/0ed598a27f6eceee7008deab3ff2311075a2ec69/compiler_tester/src/test/case/input/value.rs#L43-L146 + fn resolve( + self, + deployed_contracts: &HashMap, + chain_state_provider: &impl ResolverApi, + ) -> anyhow::Result> { + match self { + Self::Item(item) => { + let item = item.as_ref(); + let value = if let Some(instance) = item.strip_suffix(Self::ADDRESS_VARIABLE_SUFFIX) + { + Ok(U256::from_be_slice( + deployed_contracts + .get(&ContractInstance::new(instance)) + .map(|(a, _)| *a) + .ok_or_else(|| anyhow::anyhow!("Instance `{}` not found", instance))? + .as_ref(), + )) + } else if let Some(value) = item.strip_prefix(Self::NEGATIVE_VALUE_PREFIX) { + let value = U256::from_str_radix(value, 10).map_err(|error| { + anyhow::anyhow!("Invalid decimal literal after `-`: {}", error) + })?; + if value > U256::ONE << 255u8 { + anyhow::bail!("Decimal literal after `-` is too big"); + } + let value = value + .checked_sub(U256::ONE) + .ok_or_else(|| anyhow::anyhow!("`-0` is invalid literal"))?; + Ok(U256::MAX.checked_sub(value).expect("Always valid")) + } else if let Some(value) = item.strip_prefix(Self::HEX_LITERAL_PREFIX) { + Ok(U256::from_str_radix(value, 16).map_err(|error| { + anyhow::anyhow!("Invalid hexadecimal literal: {}", error) + })?) + } else if item == Self::CHAIN_VARIABLE { + let chain_id = chain_state_provider.chain_id()?; + Ok(U256::from(chain_id)) + } else if item == Self::GAS_LIMIT_VARIABLE { + let gas_limit = + chain_state_provider.block_gas_limit(BlockNumberOrTag::Latest)?; + Ok(U256::from(gas_limit)) + } else if item == Self::COINBASE_VARIABLE { + let coinbase = chain_state_provider.block_coinbase(BlockNumberOrTag::Latest)?; + Ok(U256::from_be_slice(coinbase.as_ref())) + } else if item == Self::DIFFICULTY_VARIABLE { + let block_difficulty = + chain_state_provider.block_difficulty(BlockNumberOrTag::Latest)?; + Ok(block_difficulty) + } else if item.starts_with(Self::BLOCK_HASH_VARIABLE_PREFIX) { + let offset: u64 = item + .split(':') + .next_back() + .and_then(|value| value.parse().ok()) + .unwrap_or_default(); + + let current_block_number = chain_state_provider.last_block_number()?; + let desired_block_number = current_block_number - offset; + + let block_hash = + chain_state_provider.block_hash(desired_block_number.into())?; + + Ok(U256::from_be_bytes(block_hash.0)) + } else if item == Self::BLOCK_NUMBER_VARIABLE { + let current_block_number = chain_state_provider.last_block_number()?; + Ok(U256::from(current_block_number)) + } else if item == Self::BLOCK_TIMESTAMP_VARIABLE { + let timestamp = + chain_state_provider.block_timestamp(BlockNumberOrTag::Latest)?; + Ok(U256::from(timestamp)) + } else { + Ok(U256::from_str_radix(item, 10) + .map_err(|error| anyhow::anyhow!("Invalid decimal literal: {}", error))?) + }; + value.map(CalldataToken::Item) + } + Self::Operation(operation) => Ok(CalldataToken::Operation(operation)), + } + } +} + +impl Serialize for EtherValue { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + format!("{} wei", self.0).serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for EtherValue { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let string = String::deserialize(deserializer)?; + let mut splitted = string.split(' '); + let (Some(value), Some(unit)) = (splitted.next(), splitted.next()) else { + return Err(serde::de::Error::custom("Failed to parse the value")); + }; + let parsed = parse_units(value, unit.replace("eth", "ether")) + .map_err(|_| serde::de::Error::custom("Failed to parse units"))? + .into(); + Ok(Self(parsed)) } } @@ -495,15 +687,15 @@ mod tests { .0; let input = Input { - instance: ContractInstance::new_from("Contract"), + instance: ContractInstance::new("Contract"), method: Method::FunctionName("store".to_owned()), - calldata: Calldata::Compound(vec!["42".into()]), + calldata: Calldata::new_compound(["42"]), ..Default::default() }; let mut contracts = HashMap::new(); contracts.insert( - ContractInstance::new_from("Contract"), + ContractInstance::new("Contract"), (Address::ZERO, parsed_abi), ); @@ -539,15 +731,13 @@ mod tests { let input: Input = Input { instance: "Contract".to_owned().into(), method: Method::FunctionName("send(address)".to_owned()), - calldata: Calldata::Compound(vec![ - "0x1000000000000000000000000000000000000001".to_string(), - ]), + calldata: Calldata::new_compound(["0x1000000000000000000000000000000000000001"]), ..Default::default() }; let mut contracts = HashMap::new(); contracts.insert( - ContractInstance::new_from("Contract"), + ContractInstance::new("Contract"), (Address::ZERO, parsed_abi), ); @@ -584,17 +774,15 @@ mod tests { .0; let input: Input = Input { - instance: ContractInstance::new_from("Contract"), + instance: ContractInstance::new("Contract"), method: Method::FunctionName("send".to_owned()), - calldata: Calldata::Compound(vec![ - "0x1000000000000000000000000000000000000001".to_string(), - ]), + calldata: Calldata::new_compound(["0x1000000000000000000000000000000000000001"]), ..Default::default() }; let mut contracts = HashMap::new(); contracts.insert( - ContractInstance::new_from("Contract"), + ContractInstance::new("Contract"), (Address::ZERO, parsed_abi), ); @@ -609,13 +797,21 @@ mod tests { ); } + fn resolve_calldata_item( + input: &str, + deployed_contracts: &HashMap, + chain_state_provider: &impl ResolverApi, + ) -> anyhow::Result { + CalldataItem::new(input).resolve(deployed_contracts, chain_state_provider) + } + #[test] fn resolver_can_resolve_chain_id_variable() { // Arrange let input = "$CHAIN_ID"; // Act - let resolved = resolve_argument(input, &Default::default(), &MockResolver); + let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver); // Assert let resolved = resolved.expect("Failed to resolve argument"); @@ -628,7 +824,7 @@ mod tests { let input = "$GAS_LIMIT"; // Act - let resolved = resolve_argument(input, &Default::default(), &MockResolver); + let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver); // Assert let resolved = resolved.expect("Failed to resolve argument"); @@ -644,7 +840,7 @@ mod tests { let input = "$COINBASE"; // Act - let resolved = resolve_argument(input, &Default::default(), &MockResolver); + let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver); // Assert let resolved = resolved.expect("Failed to resolve argument"); @@ -665,7 +861,7 @@ mod tests { let input = "$DIFFICULTY"; // Act - let resolved = resolve_argument(input, &Default::default(), &MockResolver); + let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver); // Assert let resolved = resolved.expect("Failed to resolve argument"); @@ -681,7 +877,7 @@ mod tests { let input = "$BLOCK_HASH"; // Act - let resolved = resolve_argument(input, &Default::default(), &MockResolver); + let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver); // Assert let resolved = resolved.expect("Failed to resolve argument"); @@ -697,7 +893,7 @@ mod tests { let input = "$BLOCK_NUMBER"; // Act - let resolved = resolve_argument(input, &Default::default(), &MockResolver); + let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver); // Assert let resolved = resolved.expect("Failed to resolve argument"); @@ -713,7 +909,7 @@ mod tests { let input = "$BLOCK_TIMESTAMP"; // Act - let resolved = resolve_argument(input, &Default::default(), &MockResolver); + let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver); // Assert let resolved = resolved.expect("Failed to resolve argument"); @@ -722,4 +918,96 @@ mod tests { U256::from(MockResolver.block_timestamp(Default::default()).unwrap()) ) } + + #[test] + fn simple_addition_can_be_resolved() { + // Arrange + let input = "2 4 +"; + + // Act + let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver); + + // Assert + let resolved = resolved.expect("Failed to resolve argument"); + assert_eq!(resolved, U256::from(6)); + } + + #[test] + fn simple_subtraction_can_be_resolved() { + // Arrange + let input = "4 2 -"; + + // Act + let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver); + + // Assert + let resolved = resolved.expect("Failed to resolve argument"); + assert_eq!(resolved, U256::from(2)); + } + + #[test] + fn simple_multiplication_can_be_resolved() { + // Arrange + let input = "4 2 *"; + + // Act + let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver); + + // Assert + let resolved = resolved.expect("Failed to resolve argument"); + assert_eq!(resolved, U256::from(8)); + } + + #[test] + fn simple_division_can_be_resolved() { + // Arrange + let input = "4 2 /"; + + // Act + let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver); + + // Assert + let resolved = resolved.expect("Failed to resolve argument"); + assert_eq!(resolved, U256::from(2)); + } + + #[test] + fn arithmetic_errors_are_not_panics() { + // Arrange + let input = "4 0 /"; + + // Act + let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver); + + // Assert + assert!(resolved.is_err()) + } + + #[test] + fn arithmetic_with_resolution_works() { + // Arrange + let input = "$BLOCK_NUMBER 10 +"; + + // Act + let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver); + + // Assert + let resolved = resolved.expect("Failed to resolve argument"); + assert_eq!( + resolved, + U256::from(MockResolver.last_block_number().unwrap() + 10) + ); + } + + #[test] + fn incorrect_number_of_arguments_errors() { + // Arrange + let input = "$BLOCK_NUMBER 10 + +"; + + // Act + let resolved = resolve_calldata_item(input, &Default::default(), &MockResolver); + + // Assert + assert!(resolved.is_err()) + } } diff --git a/crates/format/src/metadata.rs b/crates/format/src/metadata.rs index f6de7d7..9329002 100644 --- a/crates/format/src/metadata.rs +++ b/crates/format/src/metadata.rs @@ -193,10 +193,10 @@ impl Metadata { metadata.file_path = Some(path.to_path_buf()); metadata.contracts = Some( [( - ContractInstance::new_from("test"), + ContractInstance::new("test"), ContractPathAndIdentifier { contract_source_path: path.to_path_buf(), - contract_ident: ContractIdent::new_from("Test"), + contract_ident: ContractIdent::new("Test"), }, )] .into(),