Skip to main content

foundry_evm_fuzz/strategies/
calldata.rs

1use crate::{
2    FuzzFixtures,
3    strategies::{DictionaryRead, 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/// Constrains enum leaves in a decoded value to the range declared by its Solidity type.
118pub(crate) fn constrain_enum_value(
119    value: DynSolValue,
120    input: &Param,
121    fuzz_fixtures: &FuzzFixtures,
122) -> DynSolValue {
123    match EnumClamp::for_param(input, fuzz_fixtures) {
124        Some(plan) => plan.apply(value),
125        None => value,
126    }
127}
128
129/// Wraps `strat` to constrain any enum leaves in `input` to their valid range; a no-op otherwise.
130fn bound_enum(
131    strat: BoxedStrategy<DynSolValue>,
132    input: &Param,
133    fuzz_fixtures: &FuzzFixtures,
134) -> BoxedStrategy<DynSolValue> {
135    match EnumClamp::for_param(input, fuzz_fixtures) {
136        Some(plan) => strat.prop_map(move |value| plan.apply(value)).boxed(),
137        None => strat,
138    }
139}
140
141/// Recursively reduces every `uint8` leaf in an enum value (possibly nested in arrays) modulo
142/// `count`.
143fn clamp_enum_leaf(value: DynSolValue, count: U256) -> DynSolValue {
144    match value {
145        DynSolValue::Uint(v, 8) => DynSolValue::Uint(v % count, 8),
146        DynSolValue::Array(values) => {
147            DynSolValue::Array(values.into_iter().map(|v| clamp_enum_leaf(v, count)).collect())
148        }
149        DynSolValue::FixedArray(values) => {
150            DynSolValue::FixedArray(values.into_iter().map(|v| clamp_enum_leaf(v, count)).collect())
151        }
152        other => other,
153    }
154}
155
156/// Given a function, it returns a strategy which generates valid calldata
157/// for that function's input types, following declared test fixtures.
158pub fn fuzz_calldata(
159    func: Function,
160    fuzz_fixtures: &FuzzFixtures,
161) -> impl Strategy<Value = Bytes> + use<> {
162    let input_types = parse_input_types(&func);
163    // We need to compose all the strategies generated for each parameter in all
164    // possible combinations, accounting any parameter declared fixture
165    let strats = func
166        .inputs
167        .iter()
168        .zip(&input_types)
169        .map(|(input, input_type)| {
170            let strat = fuzz_param_with_fixtures(
171                input_type,
172                fuzz_fixtures.param_fixtures(&input.name),
173                &input.name,
174            );
175            bound_enum(strat, input, fuzz_fixtures)
176        })
177        .collect::<Vec<_>>();
178    let encoder = CalldataEncoder::new(func, input_types);
179    strats.prop_map(move |values| encoder.encode(&values))
180}
181
182/// Given a function and some state, it returns a strategy which generated valid calldata for the
183/// given function's input types, based on state taken from the EVM.
184pub(crate) fn fuzz_calldata_from_state<S: DictionaryRead>(
185    func: Function,
186    state: &S,
187    fuzz_fixtures: &FuzzFixtures,
188) -> impl Strategy<Value = Bytes> + use<S> {
189    let input_types = parse_input_types(&func);
190    let strats = func
191        .inputs
192        .iter()
193        .zip(&input_types)
194        .map(|(input, input_type)| {
195            let strat = fuzz_param_from_state(input_type, state);
196            bound_enum(strat, input, fuzz_fixtures)
197        })
198        .collect::<Vec<_>>();
199    let encoder = CalldataEncoder::new(func, input_types);
200    strats.prop_map(move |values| encoder.encode(&values)).no_shrink()
201}
202
203#[cfg(test)]
204mod tests {
205    use crate::{FuzzFixtures, strategies::fuzz_calldata};
206    use alloy_dyn_abi::{DynSolValue, JsonAbiExt};
207    use alloy_json_abi::Function;
208    use alloy_primitives::{Address, U256, map::HashMap};
209    use proptest::prelude::Strategy;
210
211    #[test]
212    fn can_fuzz_with_fixtures() {
213        let function = Function::parse("test_fuzzed_address(address addressFixture)").unwrap();
214
215        let address_fixture = DynSolValue::Address(Address::random());
216        let mut fixtures = HashMap::default();
217        fixtures.insert(
218            "addressFixture".to_string(),
219            DynSolValue::Array(vec![address_fixture.clone()]),
220        );
221
222        let expected = function.abi_encode_input(&[address_fixture]).unwrap();
223        let strategy = fuzz_calldata(function, &FuzzFixtures::new(fixtures));
224        let _ = strategy.prop_map(move |fuzzed| {
225            assert_eq!(expected, fuzzed);
226        });
227    }
228
229    #[test]
230    fn calldata_encoder_matches_json_abi() {
231        let function = Function::parse("test_values(uint256,string,bytes,uint64[2])").unwrap();
232        let values = vec![
233            DynSolValue::Uint(U256::from(42), 256),
234            DynSolValue::String("hello".to_string()),
235            DynSolValue::Bytes(vec![0xaa, 0xbb, 0xcc]),
236            DynSolValue::FixedArray(vec![
237                DynSolValue::Uint(U256::from(1), 64),
238                DynSolValue::Uint(U256::from(2), 64),
239            ]),
240        ];
241        let expected = function.abi_encode_input(&values).unwrap();
242        let encoder =
243            super::CalldataEncoder::new(function.clone(), super::parse_input_types(&function));
244
245        assert_eq!(expected, encoder.encode(&values));
246    }
247
248    #[test]
249    fn calldata_encoder_matches_json_abi_for_empty_inputs() {
250        let function = Function::parse("test_no_args()").unwrap();
251        let expected = function.abi_encode_input(&[]).unwrap();
252        let encoder =
253            super::CalldataEncoder::new(function.clone(), super::parse_input_types(&function));
254
255        assert_eq!(expected, encoder.encode(&[]));
256    }
257}