Skip to main content

foundry_evm_fuzz/strategies/
calldata.rs

1use crate::{
2    FuzzFixtures,
3    strategies::{FuzzStateReader, fuzz_param_from_state, fuzz_param_with_fixtures},
4};
5use alloy_dyn_abi::{DynSolCall, DynSolReturns, DynSolType, DynSolValue};
6use alloy_json_abi::{Function, Param};
7use alloy_primitives::{Bytes, U256};
8use proptest::{prelude::Strategy, strategy::BoxedStrategy};
9
10#[derive(Clone)]
11struct CalldataEncoder {
12    call: DynSolCall,
13    name: String,
14    inputs: Vec<Param>,
15}
16
17impl CalldataEncoder {
18    fn new(func: Function, input_types: Vec<DynSolType>) -> Self {
19        let selector = func.selector();
20        let name = func.name;
21        let inputs = func.inputs;
22        let call = DynSolCall::new(selector, input_types, None, DynSolReturns::new(Vec::new()));
23        Self { call, name, inputs }
24    }
25
26    fn encode(&self, values: &[DynSolValue]) -> Bytes {
27        self.call
28            .abi_encode_input(values)
29            .unwrap_or_else(|_| {
30                panic!(
31                    "Fuzzer generated invalid arguments for function `{}` with inputs {:?}: {:?}",
32                    self.name, self.inputs, values
33                )
34            })
35            .into()
36    }
37}
38
39fn parse_input_types(func: &Function) -> Vec<DynSolType> {
40    func.inputs.iter().map(|input| input.selector_type().parse().unwrap()).collect()
41}
42
43/// Plan for constraining the enum leaves of a fuzzed parameter into their valid `0..variant_count`
44/// range. Solidity enums are ABI-encoded as `uint8`, so without this the fuzzer can generate
45/// out-of-range values that the contract rejects with `Panic(0x21)` when decoding them.
46#[derive(Clone, Debug)]
47enum EnumClamp {
48    /// An enum (possibly nested in arrays); reduce every `uint8` leaf modulo the variant count.
49    Leaf(U256),
50    /// A tuple/struct; apply the per-component plans (`None` = no clamping needed).
51    Tuple(Vec<Option<Self>>),
52}
53
54impl EnumClamp {
55    /// Builds a clamp plan for `input`, returning `None` if it contains no enums to constrain.
56    fn for_param(input: &Param, fuzz_fixtures: &FuzzFixtures) -> Option<Self> {
57        // Direct enum, e.g. `EnumVal` or `EnumVal[2][]`; strip any array suffix.
58        if let Some((contract, ty)) = input.internal_type.as_ref().and_then(|it| it.as_enum()) {
59            let base = ty.split('[').next().unwrap_or(ty);
60            if let Some(count) = fuzz_fixtures.enum_variant_count(contract, base)
61                && count > 0
62            {
63                return Some(Self::Leaf(U256::from(count)));
64            }
65        }
66
67        // Struct/tuple (or `Struct[]`): recurse into components, keeping the plan only if any field
68        // needs clamping.
69        if input.components.is_empty() {
70            return None;
71        }
72        let fields = input
73            .components
74            .iter()
75            .map(|component| Self::for_param(component, fuzz_fixtures))
76            .collect::<Vec<_>>();
77        fields.iter().any(Option::is_some).then_some(Self::Tuple(fields))
78    }
79
80    /// Applies the plan to a generated value, reducing every enum leaf into its valid range.
81    fn apply(&self, value: DynSolValue) -> DynSolValue {
82        match self {
83            Self::Leaf(count) => clamp_enum_leaf(value, *count),
84            Self::Tuple(fields) => match value {
85                DynSolValue::Tuple(values) => DynSolValue::Tuple(self.apply_fields(fields, values)),
86                DynSolValue::CustomStruct { name, prop_names, tuple } => {
87                    DynSolValue::CustomStruct {
88                        name,
89                        prop_names,
90                        tuple: self.apply_fields(fields, tuple),
91                    }
92                }
93                // `Struct[]`/`Struct[N]`: apply the field plan to each element.
94                DynSolValue::Array(values) => {
95                    DynSolValue::Array(values.into_iter().map(|v| self.apply(v)).collect())
96                }
97                DynSolValue::FixedArray(values) => {
98                    DynSolValue::FixedArray(values.into_iter().map(|v| self.apply(v)).collect())
99                }
100                other => other,
101            },
102        }
103    }
104
105    fn apply_fields(&self, fields: &[Option<Self>], values: Vec<DynSolValue>) -> Vec<DynSolValue> {
106        values
107            .into_iter()
108            .enumerate()
109            .map(|(i, value)| match fields.get(i) {
110                Some(Some(plan)) => plan.apply(value),
111                _ => value,
112            })
113            .collect()
114    }
115}
116
117/// Wraps `strat` to constrain any enum leaves in `input` to their valid range; a no-op otherwise.
118fn bound_enum(
119    strat: BoxedStrategy<DynSolValue>,
120    input: &Param,
121    fuzz_fixtures: &FuzzFixtures,
122) -> BoxedStrategy<DynSolValue> {
123    match EnumClamp::for_param(input, fuzz_fixtures) {
124        Some(plan) => strat.prop_map(move |value| plan.apply(value)).boxed(),
125        None => strat,
126    }
127}
128
129/// Recursively reduces every `uint8` leaf in an enum value (possibly nested in arrays) modulo
130/// `count`.
131fn clamp_enum_leaf(value: DynSolValue, count: U256) -> DynSolValue {
132    match value {
133        DynSolValue::Uint(v, 8) => DynSolValue::Uint(v % count, 8),
134        DynSolValue::Array(values) => {
135            DynSolValue::Array(values.into_iter().map(|v| clamp_enum_leaf(v, count)).collect())
136        }
137        DynSolValue::FixedArray(values) => {
138            DynSolValue::FixedArray(values.into_iter().map(|v| clamp_enum_leaf(v, count)).collect())
139        }
140        other => other,
141    }
142}
143
144/// Given a function, it returns a strategy which generates valid calldata
145/// for that function's input types, following declared test fixtures.
146pub fn fuzz_calldata(
147    func: Function,
148    fuzz_fixtures: &FuzzFixtures,
149) -> impl Strategy<Value = Bytes> + use<> {
150    let input_types = parse_input_types(&func);
151    // We need to compose all the strategies generated for each parameter in all
152    // possible combinations, accounting any parameter declared fixture
153    let strats = func
154        .inputs
155        .iter()
156        .zip(&input_types)
157        .map(|(input, input_type)| {
158            let strat = fuzz_param_with_fixtures(
159                input_type,
160                fuzz_fixtures.param_fixtures(&input.name),
161                &input.name,
162            );
163            bound_enum(strat, input, fuzz_fixtures)
164        })
165        .collect::<Vec<_>>();
166    let encoder = CalldataEncoder::new(func, input_types);
167    strats.prop_map(move |values| encoder.encode(&values))
168}
169
170/// Given a function and some state, it returns a strategy which generated valid calldata for the
171/// given function's input types, based on state taken from the EVM.
172pub fn fuzz_calldata_from_state<S: FuzzStateReader>(
173    func: Function,
174    state: &S,
175    fuzz_fixtures: &FuzzFixtures,
176) -> impl Strategy<Value = Bytes> + use<S> {
177    let input_types = parse_input_types(&func);
178    let strats = func
179        .inputs
180        .iter()
181        .zip(&input_types)
182        .map(|(input, input_type)| {
183            let strat = fuzz_param_from_state(input_type, state);
184            bound_enum(strat, input, fuzz_fixtures)
185        })
186        .collect::<Vec<_>>();
187    let encoder = CalldataEncoder::new(func, input_types);
188    strats.prop_map(move |values| encoder.encode(&values)).no_shrink()
189}
190
191#[cfg(test)]
192mod tests {
193    use crate::{FuzzFixtures, strategies::fuzz_calldata};
194    use alloy_dyn_abi::{DynSolValue, JsonAbiExt};
195    use alloy_json_abi::Function;
196    use alloy_primitives::{Address, U256, map::HashMap};
197    use proptest::prelude::Strategy;
198
199    #[test]
200    fn can_fuzz_with_fixtures() {
201        let function = Function::parse("test_fuzzed_address(address addressFixture)").unwrap();
202
203        let address_fixture = DynSolValue::Address(Address::random());
204        let mut fixtures = HashMap::default();
205        fixtures.insert(
206            "addressFixture".to_string(),
207            DynSolValue::Array(vec![address_fixture.clone()]),
208        );
209
210        let expected = function.abi_encode_input(&[address_fixture]).unwrap();
211        let strategy = fuzz_calldata(function, &FuzzFixtures::new(fixtures));
212        let _ = strategy.prop_map(move |fuzzed| {
213            assert_eq!(expected, fuzzed);
214        });
215    }
216
217    #[test]
218    fn calldata_encoder_matches_json_abi() {
219        let function = Function::parse("test_values(uint256,string,bytes,uint64[2])").unwrap();
220        let values = vec![
221            DynSolValue::Uint(U256::from(42), 256),
222            DynSolValue::String("hello".to_string()),
223            DynSolValue::Bytes(vec![0xaa, 0xbb, 0xcc]),
224            DynSolValue::FixedArray(vec![
225                DynSolValue::Uint(U256::from(1), 64),
226                DynSolValue::Uint(U256::from(2), 64),
227            ]),
228        ];
229        let expected = function.abi_encode_input(&values).unwrap();
230        let encoder =
231            super::CalldataEncoder::new(function.clone(), super::parse_input_types(&function));
232
233        assert_eq!(expected, encoder.encode(&values));
234    }
235
236    #[test]
237    fn calldata_encoder_matches_json_abi_for_empty_inputs() {
238        let function = Function::parse("test_no_args()").unwrap();
239        let expected = function.abi_encode_input(&[]).unwrap();
240        let encoder =
241            super::CalldataEncoder::new(function.clone(), super::parse_input_types(&function));
242
243        assert_eq!(expected, encoder.encode(&[]));
244    }
245}