Skip to content

Commit 1f33c6f

Browse files
leovctDaniPopes
andauthored
feat(cheatcodes): add deterministic random value generation with seed (#8622)
* feat(cheatcodes): add ability to set seed for `vm.randomUint()` * chore: move `vm.randomAddress` test to its own contract * feat(cheatcodes): add ability to set seed for `vm.randomAddress()` * feat: use global seed instead of introducing new cheatcodes * chore: clean up * chore: clean up tests * feat: add `fuzz.seed` as inline parameter in tests * chore: trim 0x prefix * chore: nit * test: update random tests * fix: inline parsing on fuzz seed * test: set seed and update random tests * chore: remove inline config for seed * chore: clean up * chore: clean up tests * test: remove deterministic tests from testdata * test: implement forgetest to test that forge test with a seed produces deterministic random values * test: fix tests * chore: clean up * test: remove seed * fix: clippy and forge-fmt * chore: clean up * chore: rename test contract * fix: lint * chore: move rng to state instead of creating a new one when calling `vm.random*` cheats * chore: nit * test: update tests * fix: clippy * chore: nit * chore: clean up * Update crates/cheatcodes/src/inspector.rs Co-authored-by: DaniPopes <[email protected]> * test: only check outputs are the same or different * chore: clean up * chore: nits --------- Co-authored-by: DaniPopes <[email protected]>
1 parent b574cdf commit 1f33c6f

File tree

7 files changed

+114
-15
lines changed

7 files changed

+114
-15
lines changed

crates/cheatcodes/src/config.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use super::Result;
22
use crate::{script::ScriptWallets, Vm::Rpc};
3-
use alloy_primitives::Address;
3+
use alloy_primitives::{Address, U256};
44
use foundry_common::{fs::normalize_path, ContractsByArtifact};
55
use foundry_compilers::{utils::canonicalize, ProjectPathsConfig};
66
use foundry_config::{
@@ -54,6 +54,8 @@ pub struct CheatsConfig {
5454
pub running_version: Option<Version>,
5555
/// Whether to enable legacy (non-reverting) assertions.
5656
pub assertions_revert: bool,
57+
/// Optional seed for the RNG algorithm.
58+
pub seed: Option<U256>,
5759
}
5860

5961
impl CheatsConfig {
@@ -93,6 +95,7 @@ impl CheatsConfig {
9395
available_artifacts,
9496
running_version,
9597
assertions_revert: config.assertions_revert,
98+
seed: config.fuzz.seed,
9699
}
97100
}
98101

@@ -221,6 +224,7 @@ impl Default for CheatsConfig {
221224
available_artifacts: Default::default(),
222225
running_version: Default::default(),
223226
assertions_revert: true,
227+
seed: None,
224228
}
225229
}
226230
}

crates/cheatcodes/src/fs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,7 @@ mod tests {
618618
root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")),
619619
..Default::default()
620620
};
621-
Cheatcodes { config: Arc::new(config), ..Default::default() }
621+
Cheatcodes::new(Arc::new(config))
622622
}
623623

624624
#[test]

crates/cheatcodes/src/inspector.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use foundry_evm_core::{
2929
InspectorExt,
3030
};
3131
use itertools::Itertools;
32+
use rand::{rngs::StdRng, Rng, SeedableRng};
3233
use revm::{
3334
interpreter::{
3435
opcode, CallInputs, CallOutcome, CallScheme, CreateInputs, CreateOutcome, EOFCreateInputs,
@@ -315,6 +316,9 @@ pub struct Cheatcodes {
315316
/// Breakpoints supplied by the `breakpoint` cheatcode.
316317
/// `char -> (address, pc)`
317318
pub breakpoints: Breakpoints,
319+
320+
/// Optional RNG algorithm.
321+
rng: Option<StdRng>,
318322
}
319323

320324
// This is not derived because calling this in `fn new` with `..Default::default()` creates a second
@@ -356,6 +360,7 @@ impl Cheatcodes {
356360
mapping_slots: Default::default(),
357361
pc: Default::default(),
358362
breakpoints: Default::default(),
363+
rng: Default::default(),
359364
}
360365
}
361366

@@ -926,6 +931,13 @@ impl Cheatcodes {
926931

927932
None
928933
}
934+
935+
pub fn rng(&mut self) -> &mut impl Rng {
936+
self.rng.get_or_insert_with(|| match self.config.seed {
937+
Some(seed) => StdRng::from_seed(seed.to_be_bytes::<32>()),
938+
None => StdRng::from_entropy(),
939+
})
940+
}
929941
}
930942

931943
impl<DB: DatabaseExt> Inspector<DB> for Cheatcodes {

crates/cheatcodes/src/utils.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,22 +55,21 @@ impl Cheatcode for ensNamehashCall {
5555
}
5656

5757
impl Cheatcode for randomUint_0Call {
58-
fn apply(&self, _state: &mut Cheatcodes) -> Result {
58+
fn apply(&self, state: &mut Cheatcodes) -> Result {
5959
let Self {} = self;
60-
// Use thread_rng to get a random number
61-
let mut rng = rand::thread_rng();
60+
let rng = state.rng();
6261
let random_number: U256 = rng.gen();
6362
Ok(random_number.abi_encode())
6463
}
6564
}
6665

6766
impl Cheatcode for randomUint_1Call {
68-
fn apply(&self, _state: &mut Cheatcodes) -> Result {
67+
fn apply(&self, state: &mut Cheatcodes) -> Result {
6968
let Self { min, max } = *self;
7069
ensure!(min <= max, "min must be less than or equal to max");
7170
// Generate random between range min..=max
72-
let mut rng = rand::thread_rng();
7371
let exclusive_modulo = max - min;
72+
let rng = state.rng();
7473
let mut random_number = rng.gen::<U256>();
7574
if exclusive_modulo != U256::MAX {
7675
let inclusive_modulo = exclusive_modulo + U256::from(1);
@@ -82,9 +81,10 @@ impl Cheatcode for randomUint_1Call {
8281
}
8382

8483
impl Cheatcode for randomAddressCall {
85-
fn apply(&self, _state: &mut Cheatcodes) -> Result {
84+
fn apply(&self, state: &mut Cheatcodes) -> Result {
8685
let Self {} = self;
87-
let addr = Address::random();
86+
let rng = state.rng();
87+
let addr = Address::random_with(rng);
8888
Ok(addr.abi_encode())
8989
}
9090
}

crates/forge/tests/cli/test_cmd.rs

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use foundry_test_utils::{
66
rpc, str,
77
util::{OutputExt, OTHER_SOLC_VERSION, SOLC_VERSION},
88
};
9+
use similar_asserts::assert_eq;
910
use std::{path::PathBuf, str::FromStr};
1011

1112
// tests that test filters are handled correctly
@@ -597,7 +598,7 @@ contract GasWaster {
597598
contract GasLimitTest is Test {
598599
function test() public {
599600
vm.createSelectFork("<rpc>");
600-
601+
601602
GasWaster waster = new GasWaster();
602603
waster.waste();
603604
}
@@ -613,7 +614,7 @@ contract GasLimitTest is Test {
613614
forgetest!(test_match_path, |prj, cmd| {
614615
prj.add_source(
615616
"dummy",
616-
r"
617+
r"
617618
contract Dummy {
618619
function testDummy() public {}
619620
}
@@ -1048,3 +1049,76 @@ Traces:
10481049
"#
10491050
]]);
10501051
});
1052+
1053+
// tests that `forge test` with a seed produces deterministic random values for uint and addresses.
1054+
forgetest_init!(deterministic_randomness_with_seed, |prj, cmd| {
1055+
prj.wipe_contracts();
1056+
prj.add_test(
1057+
"DeterministicRandomnessTest.t.sol",
1058+
r#"
1059+
import {Test, console} from "forge-std/Test.sol";
1060+
1061+
contract DeterministicRandomnessTest is Test {
1062+
1063+
function testDeterministicRandomUint() public {
1064+
console.log(vm.randomUint());
1065+
console.log(vm.randomUint());
1066+
console.log(vm.randomUint());
1067+
}
1068+
1069+
function testDeterministicRandomUintRange() public {
1070+
uint256 min = 0;
1071+
uint256 max = 1000000000;
1072+
console.log(vm.randomUint(min, max));
1073+
console.log(vm.randomUint(min, max));
1074+
console.log(vm.randomUint(min, max));
1075+
}
1076+
1077+
function testDeterministicRandomAddress() public {
1078+
console.log(vm.randomAddress());
1079+
console.log(vm.randomAddress());
1080+
console.log(vm.randomAddress());
1081+
}
1082+
}
1083+
"#,
1084+
)
1085+
.unwrap();
1086+
1087+
// Extracts the test result section from the DeterministicRandomnessTest contract output.
1088+
fn extract_test_result(out: &str) -> &str {
1089+
let start = out
1090+
.find("for test/DeterministicRandomnessTest.t.sol:DeterministicRandomnessTest")
1091+
.unwrap();
1092+
let end = out.find("Suite result: ok.").unwrap();
1093+
&out[start..end]
1094+
}
1095+
1096+
// Run the test twice with the same seed and verify the outputs are the same.
1097+
let seed1 = "0xa1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2";
1098+
cmd.args(["test", "--fuzz-seed", seed1, "-vv"]).assert_success();
1099+
let out1 = cmd.stdout_lossy();
1100+
let res1 = extract_test_result(&out1);
1101+
1102+
cmd.forge_fuse();
1103+
cmd.args(["test", "--fuzz-seed", seed1, "-vv"]).assert_success();
1104+
let out2 = cmd.stdout_lossy();
1105+
let res2 = extract_test_result(&out2);
1106+
1107+
assert_eq!(res1, res2);
1108+
1109+
// Run the test with another seed and verify the output differs.
1110+
let seed2 = "0xb1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2";
1111+
cmd.forge_fuse();
1112+
cmd.args(["test", "--fuzz-seed", seed2, "-vv"]).assert_success();
1113+
let out3 = cmd.stdout_lossy();
1114+
let res3 = extract_test_result(&out3);
1115+
assert_ne!(res3, res1);
1116+
1117+
// Run the test without a seed and verify the outputs differs once again.
1118+
cmd.forge_fuse();
1119+
cmd.args(["test", "-vv"]).assert_success();
1120+
let out4 = cmd.stdout_lossy();
1121+
let res4 = extract_test_result(&out4);
1122+
assert_ne!(res4, res1);
1123+
assert_ne!(res4, res3);
1124+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// SPDX-License-Identifier: MIT OR Apache-2.0
2+
pragma solidity 0.8.18;
3+
4+
import "ds-test/test.sol";
5+
import "cheats/Vm.sol";
6+
7+
contract RandomAddress is DSTest {
8+
Vm constant vm = Vm(HEVM_ADDRESS);
9+
10+
function testRandomAddress() public {
11+
vm.randomAddress();
12+
}
13+
}

testdata/default/cheats/RandomUint.t.sol

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,4 @@ contract RandomUint is DSTest {
2626
assertTrue(rand >= min, "rand >= min");
2727
assertTrue(rand <= max, "rand <= max");
2828
}
29-
30-
function testRandomAddress() public {
31-
vm.randomAddress();
32-
}
3329
}

0 commit comments

Comments
 (0)