Skip to content

feat: add useSeed cheatcode to set RNG seed #10698

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

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
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
20 changes: 20 additions & 0 deletions crates/cheatcodes/assets/cheatcodes.json

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

4 changes: 4 additions & 0 deletions crates/cheatcodes/spec/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2879,6 +2879,10 @@ interface Vm {
#[cheatcode(group = Utilities)]
function shuffle(uint256[] calldata array) external returns (uint256[] memory);

/// Set RNG seed.
#[cheatcode(group = Utilities)]
function setSeed(uint256 seed) external;

/// Causes the next contract creation (via new) to fail and return its initcode in the returndata buffer.
/// This allows type-safe access to the initcode payload that would be used for contract creation.
/// Example usage:
Expand Down
8 changes: 8 additions & 0 deletions crates/cheatcodes/src/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,14 @@ impl Cheatcodes {
}
}

pub fn set_seed(&mut self, seed: U256) {
let mut config = (*self.config).clone();
config.seed = Some(seed);
self.config = Arc::new(config);
self.rng = None;
self.test_runner = None;
}

/// Returns the configured prank at given depth or the first prank configured at a lower depth.
/// For example, if pranks configured for depth 1, 3 and 5, the prank for depth 4 is the one
/// configured at depth 3.
Expand Down
12 changes: 12 additions & 0 deletions crates/cheatcodes/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,18 @@ impl Cheatcode for shuffleCall {
}
}

impl Cheatcode for setSeedCall {
fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
let Self { seed } = self;
set_seed(ccx, U256::from(*seed))
}
}

fn set_seed(ccx: &mut CheatsCtxt, seed: U256) -> Result {
ccx.state.set_seed(seed);
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 {
Expand Down
1 change: 1 addition & 0 deletions testdata/cheats/Vm.sol

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

92 changes: 92 additions & 0 deletions testdata/default/cheats/Seed.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import "ds-test/test.sol";
import "cheats/Vm.sol";

contract SeedTest is DSTest {
Vm constant vm = Vm(HEVM_ADDRESS);

function testSeedAffectsRandom() public {
// Use a known seed
uint256 seed = 123456789;
vm.setSeed(seed);

// Call a foundry cheatcode to get a random value (this depends on the integration)
Copy link
Collaborator

@grandizzy grandizzy Jun 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think should only assert same values when seed set?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found the issue. It was related to how the seed was being stored in the config. Since config is wrapped in an Arc, calling Arc::make_mut only modifies the underlying data if there are no other strong references. In my case, the config was being cloned elsewhere, so make_mut was creating a new copy, and the mutation wasn't visible where it needed to be.

I fixed it by explicitly cloning the config, updating the seed, and replacing the entire Arc inside Cheatcodes. I also made sure to reset the RNG instance so the new seed takes effect.
The tests are working properly now. Let me know if you can take another look, sorry for the noise! :b

uint256 rand1 = uint256(vm.randomUint());

// Reset the seed and verify the result is the same
vm.setSeed(seed);
uint256 rand2 = uint256(vm.randomUint());

uint256 rand3 = uint256(vm.randomUint());
// If the seed is the same, the random value must be equal
assertEq(rand1, rand2);
assertTrue(rand1 != rand3);
}

function testSeedChangesRandom() public {
// Use one seed
vm.setSeed(1);
uint256 randA = uint256(vm.randomUint());

// Use a different seed
vm.setSeed(2);
uint256 randB = uint256(vm.randomUint());

// Values must be different
assertTrue(randA != randB, "Random value must be different if seed is different");
}

function testSeedAffectsShuffle() public {
// Use a known seed
uint256 seed = 123456789;
vm.setSeed(seed);

// Create two identical arrays
uint256[] memory array1 = new uint256[](5);
uint256[] memory array2 = new uint256[](5);
for (uint256 i = 0; i < 5; i++) {
array1[i] = i;
array2[i] = i;
}

// Shuffle both arrays with the same seed
array1 = vm.shuffle(array1);
vm.setSeed(seed); // Reset the seed to get the same shuffle pattern
array2 = vm.shuffle(array2);

// Compare elements - they should be identical after shuffle
for (uint256 i = 0; i < array1.length; i++) {
assertEq(array1[i], array2[i], "Arrays should be identical with same seed");
}
}

function testDifferentSeedsProduceDifferentShuffles() public {
// Create the initial array
uint256[] memory array1 = new uint256[](5);
uint256[] memory array2 = new uint256[](5);
for (uint256 i = 0; i < 5; i++) {
array1[i] = i;
array2[i] = i;
}

// Use first seed
vm.setSeed(1);
array1 = vm.shuffle(array1);

// Use second seed
vm.setSeed(2);
array2 = vm.shuffle(array2);

// Arrays should be different (we'll check at least one difference exists)
bool foundDifference = false;
for (uint256 i = 0; i < array1.length; i++) {
if (array1[i] != array2[i]) {
foundDifference = true;
break;
}
}
assertTrue(foundDifference, "Arrays should be different with different seeds");
}
}
61 changes: 61 additions & 0 deletions testdata/default/cheats/Shuffle.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import "ds-test/test.sol";
import "cheats/Vm.sol";

contract ShuffleTest is DSTest {
Vm constant vm = Vm(HEVM_ADDRESS);

function testDeterministicShuffle() public {
// Use a known seed
uint256 seed = 123456789;
vm.setSeed(seed);

// Create two identical arrays
uint256[] memory array1 = new uint256[](5);
uint256[] memory array2 = new uint256[](5);
for (uint256 i = 0; i < 5; i++) {
array1[i] = i;
array2[i] = i;
}

// Shuffle both arrays with the same seed
array1 = vm.shuffle(array1);
vm.setSeed(seed); // Reset the seed to get the same shuffle pattern
array2 = vm.shuffle(array2);

// Compare elements - they should be identical after shuffle
for (uint256 i = 0; i < array1.length; i++) {
assertEq(array1[i], array2[i], "Arrays should be identical with same seed");
}
}

function testDifferentSeedsProduceDifferentShuffles() public {
// Create the initial array
uint256[] memory array1 = new uint256[](5);
uint256[] memory array2 = new uint256[](5);
for (uint256 i = 0; i < 5; i++) {
array1[i] = i;
array2[i] = i;
}

// Use first seed
vm.setSeed(1);
array1 = vm.shuffle(array1);

// Use second seed
vm.setSeed(2);
array2 = vm.shuffle(array2);

// Arrays should be different (we'll check at least one difference exists)
bool foundDifference = false;
for (uint256 i = 0; i < array1.length; i++) {
if (array1[i] != array2[i]) {
foundDifference = true;
break;
}
}
assertTrue(foundDifference, "Arrays should be different with different seeds");
}
}