foundry_evm_fuzz/strategies/
calldata.rs1use 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#[derive(Clone, Debug)]
47enum EnumClamp {
48 Leaf(U256),
50 Tuple(Vec<Option<Self>>),
52}
53
54impl EnumClamp {
55 fn for_param(input: &Param, fuzz_fixtures: &FuzzFixtures) -> Option<Self> {
57 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 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 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 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
117fn 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
129fn 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
144pub 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 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
170pub 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}