Skip to content

feat(cheatcodes): random* cheatcodes to aid in symbolic testing #8882

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions crates/cheatcodes/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,4 @@ thiserror.workspace = true
toml = { workspace = true, features = ["preserve_order"] }
tracing.workspace = true
walkdir.workspace = true

[dev-dependencies]
proptest.workspace = true
102 changes: 101 additions & 1 deletion crates/cheatcodes/assets/cheatcodes.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 21 additions & 1 deletion crates/cheatcodes/spec/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2337,14 +2337,34 @@ interface Vm {
#[cheatcode(group = Utilities)]
function randomUint() external returns (uint256);

/// Returns random uin256 value between the provided range (=min..=max).
/// Returns random uint256 value between the provided range (=min..=max).
#[cheatcode(group = Utilities)]
function randomUint(uint256 min, uint256 max) external returns (uint256);

/// Returns an random `uint256` value of given bits.
#[cheatcode(group = Utilities)]
function randomUint(uint256 bits) external view returns (uint256);

/// Returns a random `address`.
#[cheatcode(group = Utilities)]
function randomAddress() external returns (address);

/// Returns an random `int256` value.
#[cheatcode(group = Utilities)]
function randomInt() external view returns (int256);

/// Returns an random `int256` value of given bits.
#[cheatcode(group = Utilities)]
function randomInt(uint256 bits) external view returns (int256);

/// Returns an random `bool`.
#[cheatcode(group = Utilities)]
function randomBool() external view returns (bool);

/// Returns an random byte array value of the given length.
#[cheatcode(group = Utilities)]
function randomBytes(uint256 len) external view returns (bytes memory);

/// Pauses collection of call traces. Useful in cases when you want to skip tracing of
/// complex calls which are not useful for debugging.
#[cheatcode(group = Utilities)]
Expand Down
23 changes: 16 additions & 7 deletions crates/cheatcodes/src/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ use foundry_evm_core::{
};
use foundry_evm_traces::TracingInspector;
use itertools::Itertools;
use rand::{rngs::StdRng, Rng, SeedableRng};
use proptest::test_runner::{RngAlgorithm, TestRng, TestRunner};
use rand::Rng;
use revm::{
interpreter::{
opcode as op, CallInputs, CallOutcome, CallScheme, CreateInputs, CreateOutcome,
Expand Down Expand Up @@ -439,8 +440,9 @@ pub struct Cheatcodes {
/// `char -> (address, pc)`
pub breakpoints: Breakpoints,

/// Optional RNG algorithm.
rng: Option<StdRng>,
/// Optional cheatcodes `TestRunner`. Used for generating random values from uint and int
/// strategies.
test_runner: Option<TestRunner>,

/// Ignored traces.
pub ignored_traces: IgnoredTraces,
Expand Down Expand Up @@ -491,7 +493,7 @@ impl Cheatcodes {
mapping_slots: Default::default(),
pc: Default::default(),
breakpoints: Default::default(),
rng: Default::default(),
test_runner: Default::default(),
ignored_traces: Default::default(),
arbitrary_storage: Default::default(),
deprecated: Default::default(),
Expand Down Expand Up @@ -1068,9 +1070,16 @@ impl Cheatcodes {
}

pub fn rng(&mut self) -> &mut impl Rng {
self.rng.get_or_insert_with(|| match self.config.seed {
Some(seed) => StdRng::from_seed(seed.to_be_bytes::<32>()),
None => StdRng::from_entropy(),
self.test_runner().rng()
}

pub fn test_runner(&mut self) -> &mut TestRunner {
self.test_runner.get_or_insert_with(|| match self.config.seed {
Some(seed) => TestRunner::new_with_rng(
proptest::test_runner::Config::default(),
TestRng::from_seed(RngAlgorithm::ChaCha, &seed.to_be_bytes::<32>()),
),
None => TestRunner::new(proptest::test_runner::Config::default()),
})
}

Expand Down
117 changes: 96 additions & 21 deletions crates/cheatcodes/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
//! Implementations of [`Utilities`](spec::Group::Utilities) cheatcodes.

use crate::{Cheatcode, Cheatcodes, CheatsCtxt, Result, Vm::*};
use alloy_primitives::{Address, U256};
use alloy_dyn_abi::{DynSolType, DynSolValue};
use alloy_primitives::U256;
use alloy_sol_types::SolValue;
use foundry_common::ens::namehash;
use foundry_evm_core::{backend::DatabaseExt, constants::DEFAULT_CREATE2_DEPLOYER};
use rand::Rng;
use proptest::strategy::{Strategy, ValueTree};
use rand::{Rng, RngCore};
use std::collections::HashMap;

/// Contains locations of traces ignored via cheatcodes.
Expand Down Expand Up @@ -71,36 +73,64 @@ impl Cheatcode for ensNamehashCall {

impl Cheatcode for randomUint_0Call {
fn apply(&self, state: &mut Cheatcodes) -> Result {
let Self {} = self;
let rng = state.rng();
let random_number: U256 = rng.gen();
Ok(random_number.abi_encode())
random_uint(state, None, None)
}
}

impl Cheatcode for randomUint_1Call {
fn apply(&self, state: &mut Cheatcodes) -> Result {
let Self { min, max } = *self;
ensure!(min <= max, "min must be less than or equal to max");
// Generate random between range min..=max
let exclusive_modulo = max - min;
let rng = state.rng();
let mut random_number = rng.gen::<U256>();
if exclusive_modulo != U256::MAX {
let inclusive_modulo = exclusive_modulo + U256::from(1);
random_number %= inclusive_modulo;
}
random_number += min;
Ok(random_number.abi_encode())
random_uint(state, None, Some((min, max)))
}
}

impl Cheatcode for randomUint_2Call {
fn apply(&self, state: &mut Cheatcodes) -> Result {
let Self { bits } = *self;
random_uint(state, Some(bits), None)
}
}

impl Cheatcode for randomAddressCall {
fn apply(&self, state: &mut Cheatcodes) -> Result {
let Self {} = self;
let rng = state.rng();
let addr = Address::random_with(rng);
Ok(addr.abi_encode())
Ok(DynSolValue::type_strategy(&DynSolType::Address)
.new_tree(state.test_runner())
.unwrap()
.current()
.abi_encode())
}
}

impl Cheatcode for randomInt_0Call {
fn apply(&self, state: &mut Cheatcodes) -> Result {
random_int(state, None)
}
}

impl Cheatcode for randomInt_1Call {
fn apply(&self, state: &mut Cheatcodes) -> Result {
let Self { bits } = *self;
random_int(state, Some(bits))
}
}

impl Cheatcode for randomBoolCall {
fn apply(&self, state: &mut Cheatcodes) -> Result {
let rand_bool: bool = state.rng().gen();
Ok(rand_bool.abi_encode())
}
}

impl Cheatcode for randomBytesCall {
fn apply(&self, state: &mut Cheatcodes) -> Result {
let Self { len } = *self;
ensure!(
len <= U256::from(usize::MAX),
format!("bytes length cannot exceed {}", usize::MAX)
);
let mut bytes = vec![0u8; len.to::<usize>()];
state.rng().fill_bytes(&mut bytes);
Ok(bytes.abi_encode())
}
}

Expand Down Expand Up @@ -181,3 +211,48 @@ impl Cheatcode for copyStorageCall {
Ok(Default::default())
}
}

/// Helper to generate a random `uint` value (with given bits or bounded if specified)
/// from type strategy.
fn random_uint(state: &mut Cheatcodes, bits: Option<U256>, bounds: Option<(U256, U256)>) -> Result {
if let Some(bits) = bits {
// Generate random with specified bits.
ensure!(bits <= U256::from(256), "number of bits cannot exceed 256");
return Ok(DynSolValue::type_strategy(&DynSolType::Uint(bits.to::<usize>()))
.new_tree(state.test_runner())
.unwrap()
.current()
.abi_encode())
}

if let Some((min, max)) = bounds {
ensure!(min <= max, "min must be less than or equal to max");
// Generate random between range min..=max
let exclusive_modulo = max - min;
let mut random_number: U256 = state.rng().gen();
if exclusive_modulo != U256::MAX {
let inclusive_modulo = exclusive_modulo + U256::from(1);
random_number %= inclusive_modulo;
}
random_number += min;
return Ok(random_number.abi_encode())
}

// Generate random `uint256` value.
Ok(DynSolValue::type_strategy(&DynSolType::Uint(256))
.new_tree(state.test_runner())
.unwrap()
.current()
.abi_encode())
}

/// Helper to generate a random `int` value (with given bits if specified) from type strategy.
fn random_int(state: &mut Cheatcodes, bits: Option<U256>) -> Result {
let no_bits = bits.unwrap_or(U256::from(256));
ensure!(no_bits <= U256::from(256), "number of bits cannot exceed 256");
Ok(DynSolValue::type_strategy(&DynSolType::Int(no_bits.to::<usize>()))
.new_tree(state.test_runner())
.unwrap()
.current()
.abi_encode())
}
5 changes: 5 additions & 0 deletions testdata/cheats/Vm.sol

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading