diff --git a/crates/dyn-abi/Cargo.toml b/crates/dyn-abi/Cargo.toml index 0758ceae3..019fb89b1 100644 --- a/crates/dyn-abi/Cargo.toml +++ b/crates/dyn-abi/Cargo.toml @@ -29,17 +29,35 @@ derive_more = { workspace = true, optional = true } serde = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } +# arbitrary +arbitrary = { workspace = true, optional = true } +derive_arbitrary = { workspace = true, optional = true } +proptest = { workspace = true, optional = true } + [dev-dependencies] hex-literal.workspace = true criterion.workspace = true ethabi = "18" +rand = "0.8" [features] default = ["std"] std = ["alloy-sol-types/std", "alloy-primitives/std", "hex/std", "serde?/std", "serde_json?/std"] eip712 = ["alloy-sol-types/eip712-serde", "dep:derive_more", "dep:serde", "dep:serde_json"] +arbitrary = [ + "std", + "alloy-sol-types/arbitrary", + "dep:arbitrary", + "dep:derive_arbitrary", + "dep:proptest", +] [[bench]] name = "abi" path = "benches/abi.rs" harness = false + +[[bench]] +name = "types" +path = "benches/types.rs" +harness = false diff --git a/crates/dyn-abi/README.md b/crates/dyn-abi/README.md index 1b372191d..dacd99b07 100644 --- a/crates/dyn-abi/README.md +++ b/crates/dyn-abi/README.md @@ -13,28 +13,42 @@ The dynamic encoder/decoder is significantly more expensive, especially for complex types. It is also significantly more error prone, as the mapping between solidity types and rust types is not enforced by the compiler. -[abi]: https://docs.rs/alloy-sol-types/latest/alloy_sol_types/ +[abi]: https://docs.rs/alloy-sol-types -## Usage +## Examples + +Basic usage: ```rust use alloy_dyn_abi::{DynSolType, DynSolValue}; +use alloy_primitives::hex; // parse a type from a string -// limitation: custom structs cannot currently be parsed this way. -let my_type: DynSolType = "uint8[2][]".parse().unwrap(); - -// set values -let uints = DynSolValue::FixedArray(vec![0u8.into(), 1u8.into()]); -let my_values = DynSolValue::Array(vec![uints]); - -// encode -let encoded = my_values.clone().encode_single(); +// note: eip712 `CustomStruct`s cannot be parsed this way. +let my_type: DynSolType = "uint16[2][]".parse().unwrap(); // decode -let decoded = my_type.decode_single(&encoded).unwrap(); +let my_data = hex!( + "0000000000000000000000000000000000000000000000000000000000000020" // offset + "0000000000000000000000000000000000000000000000000000000000000001" // length + "0000000000000000000000000000000000000000000000000000000000000002" // .[0][0] + "0000000000000000000000000000000000000000000000000000000000000003" // .[0][1] +); +let decoded = my_type.decode_single(&my_data)?; + +let expected = DynSolValue::Array(vec![DynSolValue::FixedArray(vec![2u16.into(), 3u16.into()])]); +assert_eq!(decoded, expected); + +// roundtrip +let encoded = decoded.encode_single(); +assert_eq!(encoded, my_data); +# Ok::<(), alloy_dyn_abi::Error>(()) +``` + +EIP-712: -assert_eq!(decoded, my_values); +```rust,ignore +todo!() ``` ## How it works diff --git a/crates/dyn-abi/benches/abi.rs b/crates/dyn-abi/benches/abi.rs index 7b9b6d22f..0a8f6d528 100644 --- a/crates/dyn-abi/benches/abi.rs +++ b/crates/dyn-abi/benches/abi.rs @@ -13,8 +13,8 @@ fn ethabi_encode(c: &mut Criterion) { g.bench_function("single", |b| { let input = encode_single_input(); b.iter(|| { - let token = ethabi::Token::String(black_box(&input).clone()); - ethabi::encode(&[token]) + let token = ethabi::Token::String(input.clone()); + ethabi::encode(&[black_box(token)]) }); }); diff --git a/crates/dyn-abi/benches/types.rs b/crates/dyn-abi/benches/types.rs new file mode 100644 index 000000000..930dc78a0 --- /dev/null +++ b/crates/dyn-abi/benches/types.rs @@ -0,0 +1,79 @@ +use alloy_dyn_abi::DynSolType; +use criterion::{ + criterion_group, criterion_main, measurement::WallTime, BenchmarkGroup, Criterion, +}; +use rand::seq::SliceRandom; +use std::{hint::black_box, time::Duration}; + +static KEYWORDS: &[&str] = &[ + "address", "bool", "string", "bytes", "bytes32", "uint", "uint256", "int", "int256", +]; +static COMPLEX: &[&str] = &[ + "((uint104,bytes,bytes8,bytes7,address,bool,address,int256,int32,bytes1,uint56,int136),uint80,uint104,address,bool,bytes14,int16,address,string,uint176,uint72,(uint120,uint192,uint256,int232,bool,bool,bool,bytes5,int56,address,uint224,int248,bytes10,int48,int8),string,string,bool,bool)", + "(address,string,(bytes,int48,bytes30,bool,address,bytes30,int48,address,bytes17,bool,uint32),bool,address,bytes28,bytes25,uint136)", + "(uint168,bytes21,address,(bytes,bool,string,address,bool,string,bytes,uint232,int128,int64,uint96,bytes7,int136),bool,uint200[5],bool,bytes,uint240,address,address,bytes15,bytes)" +]; + +fn parse(c: &mut Criterion) { + let mut g = group(c, "parse"); + let rng = &mut rand::thread_rng(); + + g.bench_function("keywords", |b| { + b.iter(|| { + let kw = KEYWORDS.choose(rng).unwrap(); + DynSolType::parse(black_box(*kw)).unwrap() + }) + }); + g.bench_function("complex", |b| { + b.iter(|| { + let complex = COMPLEX.choose(rng).unwrap(); + DynSolType::parse(black_box(*complex)).unwrap() + }) + }); + + g.finish(); +} + +fn format(c: &mut Criterion) { + let mut g = group(c, "format"); + let rng = &mut rand::thread_rng(); + + g.bench_function("keywords", |b| { + let keyword_types = KEYWORDS + .iter() + .map(|s| DynSolType::parse(s).unwrap()) + .collect::>(); + let keyword_types = keyword_types.as_slice(); + assert!(!keyword_types.is_empty()); + b.iter(|| { + let kw = unsafe { keyword_types.choose(rng).unwrap_unchecked() }; + black_box(kw).sol_type_name() + }) + }); + g.bench_function("complex", |b| { + let complex_types = COMPLEX + .iter() + .map(|s| DynSolType::parse(s).unwrap()) + .collect::>(); + let complex_types = complex_types.as_slice(); + assert!(!complex_types.is_empty()); + b.iter(|| { + let complex = unsafe { complex_types.choose(rng).unwrap_unchecked() }; + black_box(complex).sol_type_name() + }) + }); + + g.finish(); +} + +fn group<'a>(c: &'a mut Criterion, group_name: &str) -> BenchmarkGroup<'a, WallTime> { + let mut g = c.benchmark_group(group_name); + g.noise_threshold(0.03) + .warm_up_time(Duration::from_secs(1)) + .measurement_time(Duration::from_secs(3)) + .sample_size(200); + g +} + +criterion_group!(benches, parse, format); +criterion_main!(benches); diff --git a/crates/dyn-abi/src/arbitrary.rs b/crates/dyn-abi/src/arbitrary.rs new file mode 100644 index 000000000..77adcbc20 --- /dev/null +++ b/crates/dyn-abi/src/arbitrary.rs @@ -0,0 +1,667 @@ +//! Arbitrary implementations for `DynSolType` and `DynSolValue`. +//! +//! These implementations are guaranteed to be valid, including `CustomStruct` +//! identifiers. + +// TODO: remove this after updating `ruint2`. +#![allow(clippy::arc_with_non_send_sync)] + +use crate::{DynSolType, DynSolValue}; +use alloy_primitives::{Address, B256, I256, U256}; +use arbitrary::{size_hint, Unstructured}; +use core::ops::RangeInclusive; +use proptest::{ + collection::{vec as vec_strategy, VecStrategy}, + prelude::*, + strategy::{Flatten, Map, Recursive, TupleUnion, WA}, +}; + +const DEPTH: u32 = 16; +const DESIZED_SIZE: u32 = 64; +const EXPECTED_BRANCH_SIZE: u32 = 32; + +macro_rules! prop_oneof_cfg { + ($($(@[$attr:meta])* $w:expr => $x:expr,)+) => { + TupleUnion::new(($( + $(#[$attr])* + { + ($w as u32, ::alloc::sync::Arc::new($x)) + } + ),+)) + }; +} + +#[cfg(not(feature = "eip712"))] +macro_rules! tuple_type_cfg { + (($($t:ty),+ $(,)?), $c:ty $(,)?) => { + ($($t,)+) + }; +} +#[cfg(feature = "eip712")] +macro_rules! tuple_type_cfg { + (($($t:ty),+ $(,)?), $c:ty $(,)?) => { + ($($t,)+ $c) + }; +} + +#[inline] +const fn int_size(n: usize) -> usize { + let n = (n % 255) + 1; + n + (8 - (n % 8)) +} + +#[inline] +#[cfg(feature = "eip712")] +const fn ident_char(x: u8, first: bool) -> u8 { + let x = x % 64; + match x { + 0..=25 => x + b'a', + 26..=51 => (x - 26) + b'A', + 52 => b'_', + 53 => b'$', + _ => { + if first { + b'a' + } else { + (x - 54) + b'0' + } + } + } +} + +fn non_empty_vec<'a, T: arbitrary::Arbitrary<'a>>( + u: &mut Unstructured<'a>, +) -> arbitrary::Result> { + let sz = u.int_in_range(1..=16u8)?; + let mut v = Vec::with_capacity(sz as usize); + for _ in 0..sz { + v.push(u.arbitrary()?); + } + Ok(v) +} + +#[cfg(feature = "eip712")] +struct AString(String); + +#[cfg(feature = "eip712")] +impl<'a> arbitrary::Arbitrary<'a> for AString { + #[inline] + #[cfg_attr(debug_assertions, track_caller)] + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + // note: do not use u.arbitrary() with String or Vec because it's always + // too short + let len = u.int_in_range(1..=128)?; + let mut bytes = Vec::with_capacity(len); + for i in 0..len { + bytes.push(ident_char(u.arbitrary()?, i == 0)); + } + Ok(Self::new(bytes)) + } + + #[inline] + #[cfg_attr(debug_assertions, track_caller)] + fn arbitrary_take_rest(u: Unstructured<'a>) -> arbitrary::Result { + let mut bytes = u.take_rest().to_owned(); + for (i, byte) in bytes.iter_mut().enumerate() { + *byte = ident_char(*byte, i == 0); + } + Ok(Self::new(bytes)) + } + + #[inline] + fn size_hint(depth: usize) -> (usize, Option) { + String::size_hint(depth) + } +} + +#[cfg(feature = "eip712")] +impl AString { + #[inline] + #[cfg_attr(debug_assertions, track_caller)] + fn new(bytes: Vec) -> Self { + debug_assert!(core::str::from_utf8(&bytes).is_ok()); + Self(unsafe { String::from_utf8_unchecked(bytes) }) + } +} + +#[derive(Debug, derive_arbitrary::Arbitrary)] +enum Choice { + Bool, + Int, + Uint, + Address, + FixedBytes, + Bytes, + String, + + Array, + FixedArray, + Tuple, + #[cfg(feature = "eip712")] + CustomStruct, +} + +impl<'a> arbitrary::Arbitrary<'a> for DynSolType { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + match u.arbitrary::()? { + Choice::Bool => Ok(Self::Bool), + Choice::Int => u.arbitrary().map(int_size).map(Self::Int), + Choice::Uint => u.arbitrary().map(int_size).map(Self::Uint), + Choice::Address => Ok(Self::Address), + Choice::FixedBytes => Ok(Self::FixedBytes(u.int_in_range(1..=32)?)), + Choice::Bytes => Ok(Self::Bytes), + Choice::String => Ok(Self::String), + Choice::Array => u.arbitrary().map(Self::Array), + Choice::FixedArray => Ok(Self::FixedArray(u.arbitrary()?, u.int_in_range(1..=16)?)), + Choice::Tuple => non_empty_vec(u).map(Self::Tuple), + #[cfg(feature = "eip712")] + Choice::CustomStruct => { + let name = u.arbitrary::()?.0; + let (prop_names, tuple) = u + .arbitrary_iter::<(AString, DynSolType)>()? + .flatten() + .map(|(a, b)| (a.0, b)) + .unzip(); + Ok(Self::CustomStruct { + name, + prop_names, + tuple, + }) + } + } + } + + fn size_hint(depth: usize) -> (usize, Option) { + if depth == DEPTH as usize { + (0, Some(0)) + } else { + size_hint::and( + u32::size_hint(depth), + size_hint::or_all(&[usize::size_hint(depth), Self::size_hint(depth + 1)]), + ) + } + } +} + +impl<'a> arbitrary::Arbitrary<'a> for DynSolValue { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + match u.arbitrary::()? { + // re-use name and prop_names + #[cfg(feature = "eip712")] + DynSolType::CustomStruct { + name, + prop_names, + tuple, + } => Ok(Self::CustomStruct { + name, + prop_names, + tuple: tuple + .iter() + .map(|ty| Self::arbitrary_from_type(ty, u)) + .collect::>()?, + }), + t => Self::arbitrary_from_type(&t, u), + } + } + + fn size_hint(depth: usize) -> (usize, Option) { + if depth == DEPTH as usize { + (0, Some(0)) + } else { + size_hint::and( + u32::size_hint(depth), + size_hint::or_all(&[ + B256::size_hint(depth), + usize::size_hint(depth), + Self::size_hint(depth + 1), + ]), + ) + } + } +} + +// rustscript +type ValueOfStrategy = ::Value; + +type StratMap = Map) -> T>; + +type MappedWA = WA>; + +type Flat = Flatten>; + +type Rec = Recursive) -> S>; + +#[cfg(feature = "eip712")] +const IDENT_STRATEGY: &str = crate::parser::IDENT_REGEX; +#[cfg(feature = "eip712")] +type CustomStructStrategy = BoxedStrategy; + +#[cfg(feature = "eip712")] +macro_rules! custom_struct_strategy { + ($range:expr, $elem:expr) => {{ + // TODO: Avoid boxing. This is currently needed because we capture $elem + let range: RangeInclusive = $range; + let elem: BoxedStrategy = $elem; + let strat: CustomStructStrategy = range + .prop_flat_map(move |sz| { + ( + IDENT_STRATEGY, + vec_strategy(IDENT_STRATEGY, sz..=sz), + vec_strategy(elem.clone(), sz..=sz), + ) + }) + .prop_map(|(name, prop_names, tuple)| Self::CustomStruct { + name, + prop_names, + tuple, + }) + .boxed(); + strat + }}; +} + +// we must explicitly the final types of the strategies +type TypeRecurseStrategy = TupleUnion< + tuple_type_cfg![ + ( + WA>, // Basic + MappedWA, DynSolType>, // Array + MappedWA<(BoxedStrategy, RangeInclusive), DynSolType>, // FixedArray + MappedWA>, DynSolType>, // Tuple + ), + WA>, // CustomStruct + ], +>; +type TypeStrategy = Rec; + +type ValueArrayStrategy = Flat, VecStrategy>>; + +type ValueRecurseStrategy = TupleUnion< + tuple_type_cfg![ + ( + WA>, // Basic + MappedWA, // Array + MappedWA, // FixedArray + MappedWA>, DynSolValue>, // Tuple + ), + WA>, // CustomStruct + ], +>; +type ValueStrategy = Rec; + +impl proptest::arbitrary::Arbitrary for DynSolType { + type Parameters = (u32, u32, u32); + type Strategy = TypeStrategy; + + #[inline] + fn arbitrary() -> Self::Strategy { + Self::arbitrary_with((DEPTH, DESIZED_SIZE, EXPECTED_BRANCH_SIZE)) + } + + fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { + let (depth, desired_size, expected_branch_size) = args; + Self::leaf().prop_recursive(depth, desired_size, expected_branch_size, Self::recurse) + } +} + +impl DynSolType { + #[inline] + fn leaf() -> impl Strategy { + prop_oneof![ + Just(Self::Bool), + Just(Self::Address), + any::().prop_map(|x| Self::Int(int_size(x))), + any::().prop_map(|x| Self::Uint(int_size(x))), + (1..=32usize).prop_map(Self::FixedBytes), + Just(Self::Bytes), + Just(Self::String), + ] + } + + #[inline] + fn recurse(element: BoxedStrategy) -> TypeRecurseStrategy { + prop_oneof_cfg![ + 1 => element.clone(), + 2 => element.clone().prop_map(|ty| Self::Array(Box::new(ty))), + 2 => (element.clone(), 1..=16).prop_map(|(ty, sz)| Self::FixedArray(Box::new(ty), sz)), + 2 => vec_strategy(element.clone(), 1..=16).prop_map(Self::Tuple), + @[cfg(feature = "eip712")] + 1 => custom_struct_strategy!(1..=16, element), + ] + } +} + +impl proptest::arbitrary::Arbitrary for DynSolValue { + type Parameters = (u32, u32, u32); + type Strategy = ValueStrategy; + + #[inline] + fn arbitrary() -> Self::Strategy { + Self::arbitrary_with((DEPTH, DESIZED_SIZE, EXPECTED_BRANCH_SIZE)) + } + + fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { + let (depth, desired_size, expected_branch_size) = args; + Self::leaf().prop_recursive(depth, desired_size, expected_branch_size, Self::recurse) + } +} + +impl DynSolValue { + /// Generate a [`DynSolValue`] from the given [`DynSolType`]. + pub fn arbitrary_from_type( + ty: &DynSolType, + u: &mut Unstructured<'_>, + ) -> arbitrary::Result { + match ty { + DynSolType::Bool => u.arbitrary().map(Self::Bool), + DynSolType::Address => u.arbitrary().map(Self::Address), + &DynSolType::Int(sz) => u.arbitrary().map(|x| Self::Int(x, sz)), + &DynSolType::Uint(sz) => u.arbitrary().map(|x| Self::Uint(x, sz)), + &DynSolType::FixedBytes(sz) => u.arbitrary().map(|x| Self::FixedBytes(x, sz)), + DynSolType::Bytes => u.arbitrary().map(Self::Bytes), + DynSolType::String => u.arbitrary().map(Self::String), + DynSolType::Array(ty) => { + let sz = u.int_in_range(1..=16u8)?; + let mut v = Vec::with_capacity(sz as usize); + for _ in 0..sz { + v.push(Self::arbitrary_from_type(ty, u)?); + } + Ok(Self::Array(v)) + } + &DynSolType::FixedArray(ref ty, sz) => { + let mut v = Vec::with_capacity(sz); + for _ in 0..sz { + v.push(Self::arbitrary_from_type(ty, u)?); + } + Ok(Self::FixedArray(v)) + } + DynSolType::Tuple(tuple) => tuple + .iter() + .map(|ty| Self::arbitrary_from_type(ty, u)) + .collect::, _>>() + .map(Self::Tuple), + #[cfg(feature = "eip712")] + DynSolType::CustomStruct { tuple, .. } => { + let name = u.arbitrary::()?.0; + let tuple = tuple + .iter() + .map(|ty| Self::arbitrary_from_type(ty, u)) + .collect::, _>>()?; + let sz = tuple.len(); + let prop_names = (0..sz) + .map(|_| u.arbitrary::().map(|s| s.0)) + .collect::, _>>()?; + Ok(Self::CustomStruct { + name, + prop_names, + tuple, + }) + } + } + } + + // TODO: make this `SBoxedStrategy` after updating `ruint2`. + /// Create a [proptest strategy][Strategy] to generate [`DynSolValue`]s from + /// the given type. + pub fn type_strategy(ty: &DynSolType) -> BoxedStrategy { + match ty { + DynSolType::Bool => any::().prop_map(Self::Bool).boxed(), + DynSolType::Address => any::
().prop_map(Self::Address).boxed(), + &DynSolType::Int(sz) => any::().prop_map(move |x| Self::Int(x, sz)).boxed(), + &DynSolType::Uint(sz) => any::().prop_map(move |x| Self::Uint(x, sz)).boxed(), + &DynSolType::FixedBytes(sz) => any::() + .prop_map(move |x| Self::FixedBytes(x, sz)) + .boxed(), + DynSolType::Bytes => any::>().prop_map(Self::Bytes).boxed(), + DynSolType::String => any::().prop_map(Self::String).boxed(), + DynSolType::Array(ty) => { + let element = Self::type_strategy(ty); + vec_strategy(element, 1..=16).prop_map(Self::Array).boxed() + } + DynSolType::FixedArray(ty, sz) => { + let element = Self::type_strategy(ty); + vec_strategy(element, *sz) + .prop_map(Self::FixedArray) + .boxed() + } + DynSolType::Tuple(tys) => tys + .iter() + .map(Self::type_strategy) + .collect::>() + .prop_map(Self::Tuple) + .boxed(), + #[cfg(feature = "eip712")] + DynSolType::CustomStruct { tuple, .. } => { + let types = tuple.iter().map(Self::type_strategy).collect::>(); + let sz = types.len(); + (IDENT_STRATEGY, vec_strategy(IDENT_STRATEGY, sz..=sz), types) + .prop_map(|(name, prop_names, tuple)| Self::CustomStruct { + name, + prop_names, + tuple, + }) + .boxed() + } + } + } + + /// Create a [proptest strategy][Strategy] to generate [`DynSolValue`]s from + /// the given value's type. + #[inline] + pub fn value_strategy(&self) -> BoxedStrategy { + Self::type_strategy(&self.as_type().unwrap()) + } + + #[inline] + fn leaf() -> impl Strategy { + prop_oneof![ + any::().prop_map(Self::Bool), + any::
().prop_map(Self::Address), + int_strategy::().prop_map(|(x, sz)| Self::Int(x, sz)), + int_strategy::().prop_map(|(x, sz)| Self::Uint(x, sz)), + (any::(), 1..=32usize).prop_map(|(x, sz)| DynSolValue::FixedBytes(x, sz)), + any::>().prop_map(Self::Bytes), + any::().prop_map(Self::String), + ] + } + + #[inline] + fn recurse(element: BoxedStrategy) -> ValueRecurseStrategy { + prop_oneof_cfg![ + 1 => element.clone(), + 2 => Self::array_strategy(element.clone()).prop_map(Self::Array), + 2 => Self::array_strategy(element.clone()).prop_map(Self::FixedArray), + 2 => vec_strategy(element.clone(), 1..=16).prop_map(Self::Tuple), + @[cfg(feature = "eip712")] + 1 => custom_struct_strategy!(1..=16, element), + ] + } + + /// Recursive array strategy that generates same-type arrays of up to 16 + /// elements. + /// + /// NOTE: this has to be a separate function so Rust can turn the closure + /// type (`impl Fn`) into an `fn` type. + /// + /// If you manually inline this into the function above, the compiler will + /// fail with "expected fn pointer, found closure": + /// + /// ```ignore (error) + /// error[E0308]: mismatched types + /// --> crates/dyn-abi/src/arbitrary.rs:264:18 + /// | + /// 261 | / prop_oneof![ + /// 262 | | 1 => element.clone(), + /// 263 | | 2 => Self::array_strategy(element.clone()).prop_map(Self::Array), + /// 264 | | 2 => element.prop_flat_map(|x| vec_strategy(x.value_strategy(), 1..=16)).prop_map(Self::FixedArray), + /// | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected fn pointer, found closure + /// 265 | | 2 => vec_strategy(element, 1..=16).prop_map(Self::Tuple), + /// 266 | | ] + /// | |_________- arguments to this function are incorrect + /// | + /// = note: expected struct `Map, fn(DynSolValue) -> VecStrategy>>>, ...>` + /// found struct `Map, [closure@arbitrary.rs:264:40]>>, ...>` + /// ``` + #[inline] + #[allow(rustdoc::invalid_rust_codeblocks)] + fn array_strategy(element: BoxedStrategy) -> ValueArrayStrategy { + element.prop_flat_map(|x| vec_strategy(x.value_strategy(), 1..=16)) + } +} + +#[inline] +fn int_strategy() -> impl Strategy, usize)> { + (any::(), any::().prop_map(int_size)) +} + +#[cfg(test)] +mod tests { + use super::*; + #[cfg(feature = "eip712")] + use crate::parser::{is_id_continue, is_id_start, is_valid_identifier}; + + proptest! { + #![proptest_config(ProptestConfig { + cases: 1024, + ..Default::default() + })] + + #[test] + fn int_size(x: usize) { + let sz = super::int_size(x); + prop_assert!(sz > 0 && sz <= 256, "{sz}"); + prop_assert!(sz % 8 == 0, "{sz}"); + } + + #[test] + #[cfg(feature = "eip712")] + fn ident_char(x: u8) { + let start = super::ident_char(x, true); + prop_assert!(is_id_start(start as char)); + prop_assert!(is_id_continue(start as char)); + + let cont = super::ident_char(x, false); + prop_assert!(is_id_continue(cont as char)); + } + + #[test] + #[cfg(feature = "eip712")] + fn arbitrary_string(bytes: Vec) { + prop_assume!(!bytes.is_empty()); + let mut u = Unstructured::new(&bytes); + + let s = u.arbitrary::(); + prop_assume!(s.is_ok()); + + let s = s.unwrap().0; + prop_assume!(!s.is_empty()); + + prop_assert!( + is_valid_identifier(&s), + "not a valid identifier: {:?}\ndata: {}", + s, + hex::encode_prefixed(&bytes), + ); + } + + #[test] + fn arbitrary_type(bytes: Vec) { + prop_assume!(!bytes.is_empty()); + let mut u = Unstructured::new(&bytes); + let ty = u.arbitrary::(); + prop_assume!(ty.is_ok()); + type_test(ty.unwrap())?; + } + + #[test] + fn arbitrary_value(bytes: Vec) { + prop_assume!(!bytes.is_empty()); + let mut u = Unstructured::new(&bytes); + let value = u.arbitrary::(); + prop_assume!(value.is_ok()); + value_test(value.unwrap())?; + } + + #[test] + fn proptest_type(ty: DynSolType) { + type_test(ty)?; + } + + #[test] + fn proptest_value(value: DynSolValue) { + value_test(value)?; + } + } + + fn type_test(ty: DynSolType) -> Result<(), TestCaseError> { + let s = ty.sol_type_name(); + prop_assume!(!ty.has_custom_struct()); + prop_assert_eq!(DynSolType::parse(&s), Ok(ty), "type strings don't match"); + Ok(()) + } + + fn value_test(value: DynSolValue) -> Result<(), TestCaseError> { + let ty = match value.as_type() { + Some(ty) => ty, + None => { + prop_assert!(false, "generated invalid type: {value:?}"); + unreachable!() + } + }; + // this shouldn't fail after the previous assertion + let s = value.sol_type_name().unwrap(); + + prop_assert_eq!(&s, &ty.sol_type_name(), "type strings don't match"); + + match &value { + DynSolValue::Array(values) | DynSolValue::FixedArray(values) => { + prop_assert!(!values.is_empty()); + let mut values = values.iter(); + let ty = values.next().unwrap().as_type().unwrap(); + prop_assert!( + values.all(|v| ty.matches(v)), + "array elements have different types: {value:#?}", + ); + } + #[cfg(feature = "eip712")] + DynSolValue::CustomStruct { + name, + prop_names, + tuple, + } => { + prop_assert!(is_valid_identifier(name)); + prop_assert!(prop_names.iter().all(is_valid_identifier)); + prop_assert_eq!(prop_names.len(), tuple.len()); + } + _ => {} + } + + // allow this to fail if the type contains a CustomStruct + if !ty.has_custom_struct() { + let parsed = s.parse::(); + prop_assert_eq!(parsed.as_ref(), Ok(&ty), "types don't match {:?}", s); + } + + let data = value.encode_params(); + match ty.decode_params(&data) { + // skip the check if the type contains a CustomStruct, since + // decoding will not populate names + Ok(decoded) if !decoded.has_custom_struct() => prop_assert_eq!( + &decoded, + &value, + "\n\ndecoded value doesn't match {:?} ({:?})\ndata: {:?}", + s, + ty, + hex::encode_prefixed(&data), + ), + Ok(_) => {} + Err(e) => prop_assert!( + false, + "failed to decode {s:?}: {e}\nvalue: {value:?}\ndata: {:?}", + hex::encode_prefixed(&data), + ), + } + + Ok(()) + } +} diff --git a/crates/dyn-abi/src/error.rs b/crates/dyn-abi/src/error.rs index 61e2a27ec..70a891f69 100644 --- a/crates/dyn-abi/src/error.rs +++ b/crates/dyn-abi/src/error.rs @@ -1,4 +1,4 @@ -use alloc::string::{String, ToString}; +use alloc::string::String; use core::fmt; /// Dynamic ABI result type. @@ -79,15 +79,18 @@ impl From for DynAbiError { #[allow(dead_code)] impl DynAbiError { - pub(crate) fn invalid_size(ty: impl ToString) -> DynAbiError { - DynAbiError::InvalidSize(ty.to_string()) + #[inline] + pub(crate) fn invalid_size(ty: &str) -> DynAbiError { + DynAbiError::InvalidSize(ty.into()) } - pub(crate) fn invalid_type_string(ty: impl ToString) -> DynAbiError { - DynAbiError::InvalidTypeString(ty.to_string()) + #[inline] + pub(crate) fn invalid_type_string(ty: &str) -> DynAbiError { + DynAbiError::InvalidTypeString(ty.into()) } #[cfg(feature = "eip712")] + #[inline] pub(crate) fn type_mismatch( expected: crate::DynSolType, actual: &serde_json::Value, @@ -99,17 +102,20 @@ impl DynAbiError { } #[cfg(feature = "eip712")] - pub(crate) fn invalid_property_def(def: impl ToString) -> DynAbiError { - DynAbiError::InvalidPropertyDefinition(def.to_string()) + #[inline] + pub(crate) fn invalid_property_def(def: &str) -> DynAbiError { + DynAbiError::InvalidPropertyDefinition(def.into()) } #[cfg(feature = "eip712")] - pub(crate) fn missing_type(name: impl ToString) -> DynAbiError { - DynAbiError::MissingType(name.to_string()) + #[inline] + pub(crate) fn missing_type(name: &str) -> DynAbiError { + DynAbiError::MissingType(name.into()) } #[cfg(feature = "eip712")] - pub(crate) fn circular_dependency(dep: impl ToString) -> DynAbiError { - DynAbiError::CircularDependency(dep.to_string()) + #[inline] + pub(crate) fn circular_dependency(dep: &str) -> DynAbiError { + DynAbiError::CircularDependency(dep.into()) } } diff --git a/crates/dyn-abi/src/lib.rs b/crates/dyn-abi/src/lib.rs index 6175610a1..95341d431 100644 --- a/crates/dyn-abi/src/lib.rs +++ b/crates/dyn-abi/src/lib.rs @@ -28,14 +28,17 @@ #[macro_use] extern crate alloc; +#[cfg(feature = "arbitrary")] +mod arbitrary; + mod error; pub use error::{DynAbiError, DynAbiResult}; #[doc(no_inline)] pub use alloy_sol_types::{Decoder, Eip712Domain, Encoder, Error, Result, SolType, Word}; -mod r#type; -pub use r#type::DynSolType; +mod ty; +pub use ty::DynSolType; mod value; pub use value::DynSolValue; @@ -49,33 +52,3 @@ pub mod parser; pub mod eip712; #[cfg(feature = "eip712")] pub use eip712::{parser as eip712_parser, Eip712Types, PropertyDef, Resolver, TypeDef, TypedData}; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn simple_e2e() { - // parse a type from a string - let my_type: DynSolType = "uint8[2][]".parse().unwrap(); - - // set values - let uints = DynSolValue::FixedArray(vec![64u8.into(), 128u8.into()]); - let my_values = DynSolValue::Array(vec![uints]); - - // tokenize and detokenize - let tokens = my_values.tokenize(); - let detokenized = my_type.detokenize(tokens.clone()).unwrap(); - assert_eq!(detokenized, my_values); - - // encode - let encoded = my_values.clone().encode_single(); - - // decode - let mut decoder = Decoder::new(&encoded, true); - let mut decoded = my_type.empty_dyn_token(); - decoded.decode_single_populate(&mut decoder).unwrap(); - - assert_eq!(decoded, tokens); - } -} diff --git a/crates/dyn-abi/src/parser.rs b/crates/dyn-abi/src/parser.rs index 473a23dd7..c4bfd74ee 100644 --- a/crates/dyn-abi/src/parser.rs +++ b/crates/dyn-abi/src/parser.rs @@ -6,6 +6,11 @@ use crate::{DynAbiError, DynAbiResult, DynSolType}; use alloc::{boxed::Box, vec::Vec}; use core::{fmt, num::NonZeroUsize}; +/// The regular expression for a Solidity identfier. +/// +/// +pub const IDENT_REGEX: &str = "[a-zA-Z$_][a-zA-Z0-9$_]*"; + /// Returns `true` if the given character is valid at the start of a Solidity /// identfier. #[inline] @@ -24,14 +29,17 @@ pub const fn is_id_continue(c: char) -> bool { /// symbol. /// /// -#[inline] -pub fn is_valid_identifier(s: &str) -> bool { - let mut chars = s.chars(); - if let Some(first) = chars.next() { - is_id_start(first) && chars.all(is_id_continue) - } else { - false +pub fn is_valid_identifier>(s: S) -> bool { + fn is_valid_identifier(s: &str) -> bool { + let mut chars = s.chars(); + if let Some(first) = chars.next() { + is_id_start(first) && chars.all(is_id_continue) + } else { + false + } } + + is_valid_identifier(s.as_ref()) } /// A root type, with no array suffixes. Corresponds to a single, non-sequence @@ -88,46 +96,43 @@ impl<'a> RootType<'a> { /// Resolve the type string into a basic Solidity type. pub fn resolve_basic_solidity(self) -> DynAbiResult { - let type_name = self.0; - match type_name { + match self.0 { "address" => Ok(DynSolType::Address), "bool" => Ok(DynSolType::Bool), "string" => Ok(DynSolType::String), "bytes" => Ok(DynSolType::Bytes), "uint" => Ok(DynSolType::Uint(256)), "int" => Ok(DynSolType::Int(256)), - _ => { - if let Some(sz) = type_name.strip_prefix("bytes") { - if let Ok(sz) = sz.parse::() { - return if sz != 0 && sz <= 32 { - Ok(DynSolType::FixedBytes(sz)) - } else { - Err(DynAbiError::invalid_size(type_name)) + name => { + if let Some(sz) = name.strip_prefix("bytes") { + if let Ok(sz) = sz.parse() { + if sz != 0 && sz <= 32 { + return Ok(DynSolType::FixedBytes(sz)) } } + return Err(DynAbiError::invalid_size(name)) } // fast path both integer types - let (s, is_uint) = if let Some(s) = type_name.strip_prefix('u') { + let (s, is_uint) = if let Some(s) = name.strip_prefix('u') { (s, true) } else { - (type_name, false) + (name, false) }; if let Some(sz) = s.strip_prefix("int") { - if let Ok(sz) = sz.parse::() { - return if sz != 0 && sz <= 256 && sz % 8 == 0 { - if is_uint { + if let Ok(sz) = sz.parse() { + if sz != 0 && sz <= 256 && sz % 8 == 0 { + return if is_uint { Ok(DynSolType::Uint(sz)) } else { Ok(DynSolType::Int(sz)) } - } else { - Err(DynAbiError::invalid_size(type_name)) } } + Err(DynAbiError::invalid_size(name)) + } else { + Err(DynAbiError::invalid_type_string(name)) } - - Err(DynAbiError::invalid_type_string(type_name)) } } } @@ -265,6 +270,7 @@ impl AsRef for TypeStem<'_> { impl<'a> TypeStem<'a> { /// Parse a type stem from a string. + #[inline] pub fn parse(s: &'a str) -> DynAbiResult { if s.starts_with('(') || s.starts_with("tuple") { s.try_into().map(Self::Tuple) @@ -359,15 +365,15 @@ impl<'a> TypeSpecifier<'a> { .trim() .strip_suffix(']') .ok_or_else(|| DynAbiError::invalid_type_string(value))?; - - if s.is_empty() { - sizes.push(None); + let size = if s.is_empty() { + None } else { - sizes.push(Some( + Some( s.parse() .map_err(|_| DynAbiError::invalid_type_string(value))?, - )); - } + ) + }; + sizes.push(size); } sizes.reverse(); @@ -391,6 +397,7 @@ impl<'a> TypeSpecifier<'a> { } /// Resolve the type string into a basic Solidity type if possible. + #[inline] pub fn resolve_basic_solidity(&self) -> Result { let ty = self.root.resolve_basic_solidity()?; Ok(self.wrap_type(ty)) diff --git a/crates/dyn-abi/src/type.rs b/crates/dyn-abi/src/ty.rs similarity index 83% rename from crates/dyn-abi/src/type.rs rename to crates/dyn-abi/src/ty.rs index d2450d3e5..bb8f580ac 100644 --- a/crates/dyn-abi/src/type.rs +++ b/crates/dyn-abi/src/ty.rs @@ -3,6 +3,20 @@ use crate::{ }; use alloc::{borrow::Cow, boxed::Box, string::String, vec::Vec}; use alloy_sol_types::sol_data; +use core::{fmt, str::FromStr}; + +#[cfg(feature = "eip712")] +macro_rules! as_tuple { + ($ty:ident $t:tt) => { + $ty::Tuple($t) | $ty::CustomStruct { tuple: $t, .. } + }; +} +#[cfg(not(feature = "eip712"))] +macro_rules! as_tuple { + ($ty:ident $t:tt) => { + $ty::Tuple($t) + }; +} #[derive(Debug, Clone, PartialEq, Eq)] struct StructProp { @@ -10,37 +24,60 @@ struct StructProp { ty: DynSolType, } -/// A Dynamic SolType. Equivalent to an enum wrapper around all implementers of -/// [`crate::SolType`]. This is used to represent Solidity types that are not -/// known at compile time. It is used in conjunction with [`DynToken`] and -/// [`DynSolValue`] to allow for dynamic ABI encoding and decoding. +/// A dynamic Solidity type. +/// +/// Equivalent to an enum wrapper around all implementers of [`SolType`]. +/// +/// This is used to represent Solidity types that are not known at compile time. +/// It is used in conjunction with [`DynToken`] and [`DynSolValue`] to allow for +/// dynamic ABI encoding and decoding. +/// +/// # Examples +/// +/// Parsing Solidity type strings: +/// +/// ``` +/// use alloy_dyn_abi::DynSolType; +/// +/// let type_name = "(bool,address)[]"; +/// let ty = DynSolType::parse(type_name)?; +/// assert_eq!( +/// ty, +/// DynSolType::Array(Box::new(DynSolType::Tuple(vec![ +/// DynSolType::Bool, +/// DynSolType::Address, +/// ]))) +/// ); +/// assert_eq!(ty.sol_type_name(), type_name); +/// +/// // alternatively, you can use the FromStr impl +/// let ty2 = type_name.parse::()?; +/// assert_eq!(ty, ty2); +/// # Ok::<_, alloy_dyn_abi::DynAbiError>(()) +/// ``` /// -/// Users will generally want to instantiate via the [`std::str::FromStr`] impl -/// on [`DynSolType`]. This will parse a string into a [`DynSolType`]. -/// User-defined types can be instantiated directly. +/// Decoding dynamic types: /// -/// # Example /// ``` -/// # use alloy_dyn_abi::{DynSolType, DynSolValue, Result}; -/// # use alloy_primitives::U256; -/// # pub fn main() -> Result<()> { +/// use alloy_dyn_abi::{DynSolType, DynSolValue}; +/// use alloy_primitives::U256; +/// /// let my_type = DynSolType::Uint(256); -/// let my_data: DynSolValue = U256::from(183).into(); +/// let my_data: DynSolValue = U256::from(183u64).into(); /// -/// let encoded = my_data.clone().encode_single(); +/// let encoded = my_data.encode_single(); /// let decoded = my_type.decode_single(&encoded)?; /// /// assert_eq!(decoded, my_data); /// -/// let my_type = DynSolType::Array(Box::new(DynSolType::Uint(256))); +/// let my_type = DynSolType::Array(Box::new(my_type)); /// let my_data = DynSolValue::Array(vec![my_data.clone()]); /// -/// let encoded = my_data.clone().encode_single(); +/// let encoded = my_data.encode_single(); /// let decoded = my_type.decode_single(&encoded)?; /// /// assert_eq!(decoded, my_data); -/// # Ok(()) -/// # } +/// # Ok::<_, alloy_dyn_abi::Error>(()) /// ``` #[derive(Debug, Clone, PartialEq, Eq)] pub enum DynSolType { @@ -79,17 +116,89 @@ pub enum DynSolType { }, } -impl core::str::FromStr for DynSolType { +impl fmt::Display for DynSolType { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.sol_type_name()) + } +} + +impl FromStr for DynSolType { type Err = DynAbiError; #[inline] fn from_str(s: &str) -> DynAbiResult { - TypeSpecifier::try_from(s).and_then(|t| t.resolve_basic_solidity()) + Self::parse(s) } } impl DynSolType { + /// Parses a Solidity type name string into a [`DynSolType`]. + /// + /// # Examples + /// + /// ``` + /// # use alloy_dyn_abi::DynSolType; + /// let type_name = "uint256"; + /// let ty = DynSolType::parse(type_name)?; + /// assert_eq!(ty, DynSolType::Uint(256)); + /// assert_eq!(ty.sol_type_name(), type_name); + /// # Ok::<_, alloy_dyn_abi::DynAbiError>(()) + /// ``` + #[inline] + pub fn parse(s: &str) -> DynAbiResult { + TypeSpecifier::try_from(s).and_then(|t| t.resolve_basic_solidity()) + } + + /// Fallible cast to the contents of a variant. + #[inline] + pub fn as_tuple(&self) -> Option<&[Self]> { + match self { + Self::Tuple(t) => Some(t), + _ => None, + } + } + + /// Fallible cast to the contents of a variant. + #[inline] + #[allow(clippy::missing_const_for_fn)] + pub fn as_custom_struct(&self) -> Option<(&str, &[String], &[Self])> { + match self { + #[cfg(feature = "eip712")] + Self::CustomStruct { + name, + prop_names, + tuple, + } => Some((name, prop_names, tuple)), + _ => None, + } + } + + /// Returns whether this type is contains a custom struct. + #[inline] + #[allow(clippy::missing_const_for_fn)] + pub fn has_custom_struct(&self) -> bool { + #[cfg(feature = "eip712")] + { + match self { + Self::CustomStruct { .. } => true, + Self::Array(t) => t.has_custom_struct(), + Self::FixedArray(t, _) => t.has_custom_struct(), + Self::Tuple(t) => t.iter().any(Self::has_custom_struct), + _ => false, + } + } + #[cfg(not(feature = "eip712"))] + { + false + } + } + /// Check that a given [`DynSolValue`] matches this type. + /// + /// Note: this will not check any names, but just the types; e.g for + /// `CustomStruct`, when the "eip712" feature is enabled, this will only + /// check equality between the lengths and types of the tuple. pub fn matches(&self, value: &DynSolValue) -> bool { match self { Self::Address => matches!(value, DynSolValue::Address(_)), @@ -107,27 +216,26 @@ impl DynSolType { DynSolValue::FixedArray(v) if v.len() == *size && v.iter().all(|v| t.matches(v)) ), Self::Tuple(types) => match value { - #[cfg(feature = "eip712")] - DynSolValue::Tuple(tuple) | DynSolValue::CustomStruct { tuple, .. } => { - types.iter().zip(tuple).all(|(t, v)| t.matches(v)) - } - #[cfg(not(feature = "eip712"))] - DynSolValue::Tuple(tuple) => types.iter().zip(tuple).all(|(t, v)| t.matches(v)), + as_tuple!(DynSolValue tuple) => types.iter().zip(tuple).all(|(t, v)| t.matches(v)), _ => false, }, #[cfg(feature = "eip712")] Self::CustomStruct { - name, + name: _, prop_names, tuple, } => { if let DynSolValue::CustomStruct { - name: n, + name: _, prop_names: p, tuple: t, } = value { - name == n && prop_names == p && tuple.iter().zip(t).all(|(a, b)| a.matches(b)) + // check just types + prop_names.len() == tuple.len() + && prop_names.len() == p.len() + && tuple.len() == t.len() + && tuple.iter().zip(t).all(|(a, b)| a.matches(b)) } else if let DynSolValue::Tuple(v) = value { v.iter().zip(tuple).all(|(v, t)| t.matches(v)) } else { @@ -141,31 +249,31 @@ impl DynSolType { #[allow(clippy::unnecessary_to_owned)] // https://github.com/rust-lang/rust-clippy/issues/8148 pub fn detokenize(&self, token: DynToken<'_>) -> Result { match (self, token) { - (DynSolType::Address, DynToken::Word(word)) => Ok(DynSolValue::Address( + (Self::Address, DynToken::Word(word)) => Ok(DynSolValue::Address( sol_data::Address::detokenize(word.into()), )), - (DynSolType::Bool, DynToken::Word(word)) => { + (Self::Bool, DynToken::Word(word)) => { Ok(DynSolValue::Bool(sol_data::Bool::detokenize(word.into()))) } - (DynSolType::Bytes, DynToken::PackedSeq(buf)) => Ok(DynSolValue::Bytes(buf.to_vec())), - (DynSolType::FixedBytes(size), DynToken::Word(word)) => Ok(DynSolValue::FixedBytes( + (Self::Bytes, DynToken::PackedSeq(buf)) => Ok(DynSolValue::Bytes(buf.to_vec())), + (Self::FixedBytes(size), DynToken::Word(word)) => Ok(DynSolValue::FixedBytes( sol_data::FixedBytes::<32>::detokenize(word.into()).into(), *size, )), // cheating here, but it's ok - (DynSolType::Int(size), DynToken::Word(word)) => Ok(DynSolValue::Int( + (Self::Int(size), DynToken::Word(word)) => Ok(DynSolValue::Int( sol_data::Int::<256>::detokenize(word.into()), *size, )), - (DynSolType::Uint(size), DynToken::Word(word)) => Ok(DynSolValue::Uint( + (Self::Uint(size), DynToken::Word(word)) => Ok(DynSolValue::Uint( sol_data::Uint::<256>::detokenize(word.into()), *size, )), - (DynSolType::String, DynToken::PackedSeq(buf)) => Ok(DynSolValue::String( + (Self::String, DynToken::PackedSeq(buf)) => Ok(DynSolValue::String( sol_data::String::detokenize(buf.into()), )), - (DynSolType::Tuple(types), DynToken::FixedSeq(tokens, _)) => { + (Self::Tuple(types), DynToken::FixedSeq(tokens, _)) => { if types.len() != tokens.len() { return Err(crate::Error::custom( "tuple length mismatch on dynamic detokenization", @@ -178,13 +286,13 @@ impl DynSolType { .collect::>() .map(DynSolValue::Tuple) } - (DynSolType::Array(t), DynToken::DynSeq { contents, .. }) => contents + (Self::Array(t), DynToken::DynSeq { contents, .. }) => contents .into_owned() .into_iter() .map(|tok| t.detokenize(tok)) .collect::>() .map(DynSolValue::Array), - (DynSolType::FixedArray(t, size), DynToken::FixedSeq(tokens, _)) => { + (Self::FixedArray(t, size), DynToken::FixedSeq(tokens, _)) => { if *size != tokens.len() { return Err(crate::Error::custom( "array length mismatch on dynamic detokenization", @@ -199,7 +307,7 @@ impl DynSolType { } #[cfg(feature = "eip712")] ( - DynSolType::CustomStruct { + Self::CustomStruct { name, tuple, prop_names, @@ -299,35 +407,43 @@ impl DynSolType { if let Some(s) = self.sol_type_name_simple() { Cow::Borrowed(s) } else { - let mut s = String::with_capacity(64); + let capacity = match self { + Self::Tuple(_) => 256, + _ => 16, + }; + let mut s = String::with_capacity(capacity); self.sol_type_name_raw(&mut s); Cow::Owned(s) } } + /// The Solidity type name, as a `String`. + /// + /// Note: this shadows the inherent `ToString` implementation, derived from + /// [`fmt::Display`], for performance reasons. + #[inline] + #[allow(clippy::inherent_to_string_shadow_display)] + pub fn to_string(&self) -> String { + self.sol_type_name().into_owned() + } + /// Instantiate an empty dyn token, to be decoded into. pub(crate) fn empty_dyn_token(&self) -> DynToken<'_> { match self { - DynSolType::Address => DynToken::Word(Word::ZERO), - DynSolType::Bool => DynToken::Word(Word::ZERO), - DynSolType::Bytes => DynToken::PackedSeq(&[]), - DynSolType::FixedBytes(_) => DynToken::Word(Word::ZERO), - DynSolType::Int(_) => DynToken::Word(Word::ZERO), - DynSolType::Uint(_) => DynToken::Word(Word::ZERO), - DynSolType::String => DynToken::PackedSeq(&[]), - DynSolType::Tuple(types) => DynToken::FixedSeq( - types.iter().map(|t| t.empty_dyn_token()).collect(), - types.len(), - ), - DynSolType::Array(t) => DynToken::DynSeq { + Self::Address | Self::Bool | Self::FixedBytes(_) | Self::Int(_) | Self::Uint(_) => { + DynToken::Word(Word::ZERO) + } + + Self::Bytes | Self::String => DynToken::PackedSeq(&[]), + + Self::Array(t) => DynToken::DynSeq { contents: Default::default(), template: Some(Box::new(t.empty_dyn_token())), }, - DynSolType::FixedArray(t, size) => { - DynToken::FixedSeq(vec![t.empty_dyn_token(); *size].into(), *size) + &Self::FixedArray(ref t, size) => { + DynToken::FixedSeq(vec![t.empty_dyn_token(); size].into(), size) } - #[cfg(feature = "eip712")] - DynSolType::CustomStruct { tuple, .. } => DynToken::FixedSeq( + as_tuple!(Self tuple) => DynToken::FixedSeq( tuple.iter().map(|t| t.empty_dyn_token()).collect(), tuple.len(), ), @@ -358,7 +474,7 @@ impl DynSolType { #[inline] pub fn decode_params(&self, data: &[u8]) -> Result { match self { - DynSolType::Tuple(_) => self.decode_sequence(data), + Self::Tuple(_) => self.decode_sequence(data), _ => self.decode_single(data), } } @@ -477,7 +593,7 @@ mod tests { 0000000000000000000000002222222222222222222222222222222222222222 "), - fixed_array_of_dyanmic_arrays_of_addresses("address[][2]", " + fixed_array_of_dynamic_arrays_of_addresses("address[][2]", " 0000000000000000000000000000000000000000000000000000000000000020 0000000000000000000000000000000000000000000000000000000000000040 00000000000000000000000000000000000000000000000000000000000000a0 diff --git a/crates/dyn-abi/src/value.rs b/crates/dyn-abi/src/value.rs index afe22fd9b..27eee8511 100644 --- a/crates/dyn-abi/src/value.rs +++ b/crates/dyn-abi/src/value.rs @@ -16,9 +16,25 @@ macro_rules! as_fixed_seq { }; } -/// This type represents a Solidity value that has been decoded into rust. It -/// is broadly similar to `serde_json::Value` in that it is an enum of possible -/// types, and the user must inspect and disambiguate. +/// A dynamic Solidity value. +/// +/// It is broadly similar to `serde_json::Value` in that it is an enum of +/// possible types, and the user must inspect and disambiguate. +/// +/// # Examples +/// +/// ``` +/// use alloy_dyn_abi::{DynSolType, DynSolValue}; +/// +/// let my_type: DynSolType = "uint64".parse().unwrap(); +/// let my_data: DynSolValue = 183u64.into(); +/// +/// let encoded = my_data.encode_single(); +/// let decoded = my_type.decode_single(&encoded)?; +/// +/// assert_eq!(decoded, my_data); +/// # Ok::<(), alloy_dyn_abi::Error>(()) +/// ``` #[derive(Debug, Clone, PartialEq)] pub enum DynSolValue { /// An address. @@ -86,14 +102,14 @@ impl From for DynSolValue { impl From> for DynSolValue { #[inline] - fn from(value: Vec) -> Self { + fn from(value: Vec) -> Self { Self::Array(value) } } impl From<[DynSolValue; N]> for DynSolValue { #[inline] - fn from(value: [DynSolValue; N]) -> Self { + fn from(value: [Self; N]) -> Self { Self::FixedArray(value.to_vec()) } } @@ -153,7 +169,7 @@ impl DynSolValue { /// The Solidity type. This returns the solidity type corresponding to this /// value, if it is known. A type will not be known if the value contains /// an empty sequence, e.g. `T[0]`. - pub fn sol_type(&self) -> Option { + pub fn as_type(&self) -> Option { let ty = match self { Self::Address(_) => DynSolType::Address, Self::Bool(_) => DynSolType::Bool, @@ -165,13 +181,13 @@ impl DynSolValue { Self::Tuple(inner) => { return inner .iter() - .map(Self::sol_type) + .map(Self::as_type) .collect::>>() .map(DynSolType::Tuple) } - Self::Array(inner) => DynSolType::Array(Box::new(Self::sol_type(inner.first()?)?)), + Self::Array(inner) => DynSolType::Array(Box::new(Self::as_type(inner.first()?)?)), Self::FixedArray(inner) => { - DynSolType::FixedArray(Box::new(Self::sol_type(inner.first()?)?), inner.len()) + DynSolType::FixedArray(Box::new(Self::as_type(inner.first()?)?), inner.len()) } #[cfg(feature = "eip712")] Self::CustomStruct { @@ -183,7 +199,7 @@ impl DynSolValue { prop_names: prop_names.clone(), tuple: tuple .iter() - .map(Self::sol_type) + .map(Self::as_type) .collect::>>()?, }, }; @@ -232,9 +248,6 @@ impl DynSolValue { } Self::Tuple(inner) => { - if inner.is_empty() { - return false - } out.push('('); for (i, val) in inner.iter().enumerate() { if i > 0 { @@ -282,7 +295,11 @@ impl DynSolValue { if let Some(s) = self.sol_type_name_simple() { Some(Cow::Borrowed(s)) } else { - let mut s = String::with_capacity(64); + let capacity = match self { + Self::Tuple(_) => 256, + _ => 16, + }; + let mut s = String::with_capacity(capacity); if self.sol_type_name_raw(&mut s) { Some(Cow::Owned(s)) } else { @@ -382,7 +399,7 @@ impl DynSolValue { /// Fallible cast to the contents of a variant. #[inline] - pub fn as_tuple(&self) -> Option<&[DynSolValue]> { + pub fn as_tuple(&self) -> Option<&[Self]> { match self { Self::Tuple(t) => Some(t), _ => None, @@ -391,7 +408,7 @@ impl DynSolValue { /// Fallible cast to the contents of a variant. #[inline] - pub fn as_array(&self) -> Option<&[DynSolValue]> { + pub fn as_array(&self) -> Option<&[Self]> { match self { Self::Array(a) => Some(a), _ => None, @@ -400,7 +417,7 @@ impl DynSolValue { /// Fallible cast to the contents of a variant. #[inline] - pub fn as_fixed_array(&self) -> Option<&[DynSolValue]> { + pub fn as_fixed_array(&self) -> Option<&[Self]> { match self { Self::FixedArray(a) => Some(a), _ => None, @@ -409,9 +426,10 @@ impl DynSolValue { /// Fallible cast to the contents of a variant. #[inline] - #[cfg(feature = "eip712")] - pub fn as_custom_struct(&self) -> Option<(&str, &[String], &[DynSolValue])> { + #[allow(clippy::missing_const_for_fn)] + pub fn as_custom_struct(&self) -> Option<(&str, &[String], &[Self])> { match self { + #[cfg(feature = "eip712")] Self::CustomStruct { name, prop_names, @@ -421,6 +439,26 @@ impl DynSolValue { } } + /// Returns whether this type is contains a custom struct. + #[inline] + #[allow(clippy::missing_const_for_fn)] + pub fn has_custom_struct(&self) -> bool { + #[cfg(feature = "eip712")] + { + match self { + Self::CustomStruct { .. } => true, + Self::Array(t) | Self::FixedArray(t) | Self::Tuple(t) => { + t.iter().any(Self::has_custom_struct) + } + _ => false, + } + } + #[cfg(not(feature = "eip712"))] + { + false + } + } + /// Returns true if the value is a sequence type. #[inline] pub const fn is_sequence(&self) -> bool { @@ -430,7 +468,7 @@ impl DynSolValue { /// Fallible cast to a fixed-size array. Any of a `FixedArray`, a `Tuple`, /// or a `CustomStruct`. #[inline] - pub fn as_fixed_seq(&self) -> Option<&[DynSolValue]> { + pub fn as_fixed_seq(&self) -> Option<&[Self]> { match self { as_fixed_seq!(tuple) => Some(tuple), _ => None, @@ -569,7 +607,7 @@ impl DynSolValue { } Self::Array(array) => { - enc.append_seq_len(array); + enc.append_seq_len(array.len()); Self::encode_sequence(array, enc); } } diff --git a/crates/primitives/src/bits/macros.rs b/crates/primitives/src/bits/macros.rs index 9c0f7910e..40fdeb0fb 100644 --- a/crates/primitives/src/bits/macros.rs +++ b/crates/primitives/src/bits/macros.rs @@ -468,13 +468,15 @@ macro_rules! impl_arbitrary { #[inline] fn arbitrary() -> Self::Strategy { use $crate::private::proptest::strategy::Strategy; - <$crate::FixedBytes<$n> as $crate::private::proptest::arbitrary::Arbitrary>::arbitrary().prop_map(Self) + <$crate::FixedBytes<$n> as $crate::private::proptest::arbitrary::Arbitrary>::arbitrary() + .prop_map(Self) } #[inline] fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { use $crate::private::proptest::strategy::Strategy; - <$crate::FixedBytes<$n> as $crate::private::proptest::arbitrary::Arbitrary>::arbitrary_with(args).prop_map(Self) + <$crate::FixedBytes<$n> as $crate::private::proptest::arbitrary::Arbitrary>::arbitrary_with(args) + .prop_map(Self) } } }; diff --git a/crates/sol-types/Cargo.toml b/crates/sol-types/Cargo.toml index 011e95598..3ec6c2b23 100644 --- a/crates/sol-types/Cargo.toml +++ b/crates/sol-types/Cargo.toml @@ -36,3 +36,4 @@ trybuild = "1.0" default = ["std"] std = ["alloy-primitives/std", "hex/std", "serde?/std"] eip712-serde = ["dep:serde", "serde?/alloc", "alloy-primitives/serde"] +arbitrary = ["alloy-primitives/arbitrary"] diff --git a/crates/sol-types/src/coder/encoder.rs b/crates/sol-types/src/coder/encoder.rs index d07c90b7e..e2ebefbec 100644 --- a/crates/sol-types/src/coder/encoder.rs +++ b/crates/sol-types/src/coder/encoder.rs @@ -66,7 +66,12 @@ impl Encoder { } /// Determine the current suffix offset. + /// + /// # Panics + /// + /// This method panics if there is no current suffix offset. #[inline] + #[cfg_attr(debug_assertions, track_caller)] pub fn suffix_offset(&self) -> u32 { debug_assert!(!self.suffix_offset.is_empty()); unsafe { *self.suffix_offset.last().unwrap_unchecked() } @@ -99,15 +104,20 @@ impl Encoder { } /// Append a pointer to the current suffix offset. + /// + /// # Panics + /// + /// This method panics if there is no current suffix offset. #[inline] + #[cfg_attr(debug_assertions, track_caller)] pub fn append_indirection(&mut self) { self.append_word(pad_u32(self.suffix_offset())); } /// Append a sequence length. #[inline] - pub fn append_seq_len(&mut self, seq: &[T]) { - self.append_word(pad_u32(seq.len() as u32)); + pub fn append_seq_len(&mut self, len: usize) { + self.append_word(pad_u32(len as u32)); } /// Append a sequence of bytes, padding to the next word. @@ -134,7 +144,7 @@ impl Encoder { /// Append a sequence of bytes as a packed sequence with a length prefix. #[inline] pub fn append_packed_seq(&mut self, bytes: &[u8]) { - self.append_seq_len(bytes); + self.append_seq_len(bytes.len()); self.append_bytes(bytes); } diff --git a/crates/sol-types/src/coder/token.rs b/crates/sol-types/src/coder/token.rs index 28ba9bb1c..a2e073fd6 100644 --- a/crates/sol-types/src/coder/token.rs +++ b/crates/sol-types/src/coder/token.rs @@ -356,7 +356,7 @@ impl<'de, T: TokenType<'de>> TokenType<'de> for DynSeqToken { #[inline] fn tail_append(&self, enc: &mut Encoder) { - enc.append_seq_len(&self.0); + enc.append_seq_len(self.0.len()); self.encode_sequence(enc); } }