Skip to main content

foundry_evm_fuzz/strategies/
uint.rs

1use alloy_dyn_abi::{DynSolType, DynSolValue};
2use alloy_primitives::U256;
3use proptest::{
4    prelude::Rng,
5    strategy::{NewTree, Strategy, ValueTree},
6    test_runner::TestRunner,
7};
8
9/// Value tree for unsigned ints (up to uint256).
10pub struct UintValueTree {
11    /// Lower base
12    lo: U256,
13    /// Current value
14    curr: U256,
15    /// Higher base
16    hi: U256,
17    /// If true cannot be simplified or complexified
18    fixed: bool,
19}
20
21impl UintValueTree {
22    /// Create a new tree
23    /// # Arguments
24    /// * `start` - Starting value for the tree
25    /// * `fixed` - If `true` the tree would only contain one element and won't be simplified.
26    fn new(start: U256, fixed: bool) -> Self {
27        Self { lo: U256::ZERO, curr: start, hi: start, fixed }
28    }
29
30    fn reposition(&mut self) -> bool {
31        let interval = self.hi - self.lo;
32        let new_mid = self.lo + interval / U256::from(2);
33
34        if new_mid == self.curr {
35            false
36        } else {
37            self.curr = new_mid;
38            true
39        }
40    }
41}
42
43impl ValueTree for UintValueTree {
44    type Value = U256;
45
46    fn current(&self) -> Self::Value {
47        self.curr
48    }
49
50    fn simplify(&mut self) -> bool {
51        if self.fixed || (self.hi <= self.lo) {
52            return false;
53        }
54        self.hi = self.curr;
55        self.reposition()
56    }
57
58    fn complicate(&mut self) -> bool {
59        if self.fixed || (self.hi <= self.lo) {
60            return false;
61        }
62
63        self.lo = self.curr + U256::from(1);
64        self.reposition()
65    }
66}
67
68/// Value tree for unsigned ints (up to uint256).
69/// The strategy combines 3 different strategies, each assigned a specific weight:
70/// 1. Generate purely random value in a range. This will first choose bit size uniformly (up `bits`
71///    param). Then generate a value for this bit size.
72/// 2. Generate a random value around the edges (+/- 3 around 0 and max possible value)
73/// 3. Generate a value from a predefined fixtures set
74///
75/// To define uint fixtures:
76/// - return an array of possible values for a parameter named `amount` declare a function `function
77///   fixture_amount() public returns (uint32[] memory)`.
78/// - use `amount` named parameter in fuzzed test in order to include fixtures in fuzzed values
79///   `function testFuzz_uint32(uint32 amount)`.
80///
81/// If fixture is not a valid uint type then error is raised and random value generated.
82#[derive(Debug)]
83pub struct UintStrategy {
84    /// Bit size of uint (e.g. 256)
85    bits: usize,
86    /// A set of fixtures to be generated
87    fixtures: Vec<DynSolValue>,
88    /// The weight for edge cases (+/- 3 around 0 and max possible value)
89    edge_weight: usize,
90    /// The weight for fixtures
91    fixtures_weight: usize,
92    /// The weight for purely random values
93    random_weight: usize,
94    /// Optional maximum value for generated integers, used to simulate smaller unsigned types.
95    /// When set, generated values will be clamped to [0, max_value].
96    max_value: Option<U256>,
97}
98
99impl UintStrategy {
100    /// Create a new strategy.
101    /// # Arguments
102    /// * `bits` - Size of uint in bits
103    /// * `fixtures` - A set of fixed values to be generated (according to fixtures weight)
104    /// * `max_value` - Optional maximum value to simulate smaller unsigned types. Values will be
105    ///   clamped to [0, max_value].
106    pub fn new(bits: usize, fixtures: Option<&[DynSolValue]>, max_value: Option<U256>) -> Self {
107        Self {
108            bits,
109            fixtures: Vec::from(fixtures.unwrap_or_default()),
110            edge_weight: 10usize,
111            fixtures_weight: 40usize,
112            random_weight: 50usize,
113            max_value,
114        }
115    }
116
117    fn generate_edge_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
118        let rng = runner.rng();
119        // Choose if we want values around 0 or max
120        let is_min = rng.random::<bool>();
121        let offset = U256::from(rng.random_range(0..4));
122        let start = if is_min { offset } else { self.type_max().saturating_sub(offset) };
123        Ok(UintValueTree::new(start, false))
124    }
125
126    fn generate_fixtures_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
127        // generate random cases if there's no fixtures
128        if self.fixtures.is_empty() {
129            return self.generate_random_tree(runner);
130        }
131
132        // Generate value tree from fixture.
133        let fixture = &self.fixtures[runner.rng().random_range(0..self.fixtures.len())];
134        if let Some(uint_fixture) = fixture.as_uint()
135            && uint_fixture.1 == self.bits
136        {
137            let value = uint_fixture.0.min(self.type_max());
138            return Ok(UintValueTree::new(value, false));
139        }
140
141        // If fixture is not a valid type, raise error and generate random value.
142        error!("{:?} is not a valid {} fixture", fixture, DynSolType::Uint(self.bits));
143        self.generate_random_tree(runner)
144    }
145
146    fn generate_random_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
147        let rng = runner.rng();
148
149        // generate random number of bits uniformly
150        let bits = rng.random_range(0..=self.bits);
151
152        // init 2 128-bit randoms
153        let mut higher: u128 = rng.random_range(0..=u128::MAX);
154        let mut lower: u128 = rng.random_range(0..=u128::MAX);
155
156        // cut 2 randoms according to bits size
157        match bits {
158            x if x < 128 => {
159                lower &= (1u128 << x) - 1;
160                higher = 0;
161            }
162            x if (128..256).contains(&x) => higher &= (1u128 << (x - 128)) - 1,
163            _ => {}
164        };
165
166        // init U256 from 2 randoms
167        let mut inner: [u64; 4] = [0; 4];
168        let mask64 = (1 << 65) - 1;
169        inner[0] = (lower & mask64) as u64;
170        inner[1] = (lower >> 64) as u64;
171        inner[2] = (higher & mask64) as u64;
172        inner[3] = (higher >> 64) as u64;
173        let start: U256 = U256::from_limbs(inner);
174
175        // Clamp to max_value if set
176        let start = start.min(self.type_max());
177
178        Ok(UintValueTree::new(start, false))
179    }
180
181    fn type_max(&self) -> U256 {
182        let type_max =
183            if self.bits < 256 { (U256::from(1) << self.bits) - U256::from(1) } else { U256::MAX };
184        self.max_value.map(|m| type_max.min(m)).unwrap_or(type_max)
185    }
186}
187
188impl Strategy for UintStrategy {
189    type Tree = UintValueTree;
190    type Value = U256;
191    fn new_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
192        let total_weight = self.random_weight + self.fixtures_weight + self.edge_weight;
193        let bias = runner.rng().random_range(0..total_weight);
194        // randomly select one of 3 strategies
195        match bias {
196            x if x < self.edge_weight => self.generate_edge_tree(runner),
197            x if x < self.edge_weight + self.fixtures_weight => self.generate_fixtures_tree(runner),
198            _ => self.generate_random_tree(runner),
199        }
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use crate::strategies::uint::UintValueTree;
206    use alloy_primitives::U256;
207    use proptest::strategy::ValueTree;
208
209    #[test]
210    fn test_uint_tree_complicate_max() {
211        let mut uint_tree = UintValueTree::new(U256::MAX, false);
212        assert_eq!(uint_tree.hi, U256::MAX);
213        assert_eq!(uint_tree.curr, U256::MAX);
214        uint_tree.complicate();
215        assert_eq!(uint_tree.lo, U256::MIN);
216    }
217}