Repository: foundry-rs/foundry (language: rust)

Issue:
feat(`cheatcodes`): add `vm.foundryVersionAtLeast` + `vm.foundryVersionCmp` to make it easier to write conditional version logic
Followup from https://github.com/foundry-rs/foundry/pull/9683

`vm.getFoundryVersion` will now use the format `forge 0.3.0-dev+b3d0002118.1737037945.debug`

`<cargo_version>-<tag>+<git_sha_short>.<unix_build_timestamp>.<profile>`

We should provide a function that does the comparison directly like `vm.versionAtLeast("1.2.3")` and/or `vm.versionTimestampAtLeast(unix)`

_Originally posted by @DaniPopes in https://github.com/foundry-rs/foundry/pull/9683#discussion_r1919053389_
            


Discussion / hints:
I'd like to handle this task.
I think we want to prefix with foundry so `foundryVersion`

Another variation is `foundryVersionCmp(string) returns(int)` which returns -1, 0, 1 for less, equal, greater than; then you then can do <, <=, ==, >=, > with 0 to do the comparison

This is implemented as parsing the string input as semver, getting the Ordering from parsed.cmp(FOUNDRY_VERSION), and returning it as isize
foundry does not build hermetically across OS's, setting a defined timestamp would fix the issue w.r.t. different binaries having different build timestamps but coming from the same commit
Could I take over this issue?
Mind if I take this? I've already made a rough implementation locally for these cheat codes.

Fix patch:
diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json
index cb49f2de1a27..ac53a1f56476 100644
--- a/crates/cheatcodes/assets/cheatcodes.json
+++ b/crates/cheatcodes/assets/cheatcodes.json
@@ -5472,6 +5472,46 @@
       "status": "stable",
       "safety": "safe"
     },
+    {
+      "func": {
+        "id": "foundryVersionAtLeast",
+        "description": "Returns true if the current Foundry version is at least the given version.\nVersion string can be in the format `major.minor.patch`.",
+        "declaration": "function foundryVersionAtLeast(string calldata version) external view returns (bool);",
+        "visibility": "external",
+        "mutability": "view",
+        "signature": "foundryVersionAtLeast(string)",
+        "selector": "0x6248be1f",
+        "selectorBytes": [
+          98,
+          72,
+          190,
+          31
+        ]
+      },
+      "group": "testing",
+      "status": "stable",
+      "safety": "safe"
+    },
+    {
+      "func": {
+        "id": "foundryVersionCmp",
+        "description": "Compares the current Foundry version with the given version string.\nVersion string can be in the format `major.minor.patch`.\nReturns:\n-1 if current version is less than the given version\n0 if current version equals the given version\n1 if current version is greater than the given version",
+        "declaration": "function foundryVersionCmp(string calldata version) external view returns (int256);",
+        "visibility": "external",
+        "mutability": "view",
+        "signature": "foundryVersionCmp(string)",
+        "selector": "0xca7b0a09",
+        "selectorBytes": [
+          202,
+          123,
+          10,
+          9
+        ]
+      },
+      "group": "testing",
+      "status": "stable",
+      "safety": "safe"
+    },
     {
       "func": {
         "id": "fsMetadata",
diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs
index cf9709276335..29fb986768d8 100644
--- a/crates/cheatcodes/spec/src/vm.rs
+++ b/crates/cheatcodes/spec/src/vm.rs
@@ -1677,6 +1677,21 @@ interface Vm {
         string calldata error
     ) external pure;
 
+    /// Returns true if the current Foundry version is at least the given version.
+    /// Version string can be in the format `major.minor.patch`.
+    #[cheatcode(group = Testing, safety = Safe)]
+    function foundryVersionAtLeast(string calldata version) external view returns (bool);
+
+    /// Compares the current Foundry version with the given version string.
+    /// Version string can be in the format `major.minor.patch`.
+    /// Returns:
+    /// -1 if current version is less than the given version
+    /// 0 if current version equals the given version
+    /// 1 if current version is greater than the given version
+    #[cheatcode(group = Testing, safety = Safe)]
+    function foundryVersionCmp(string calldata version) external view returns (int256);
+
+
     // ======== OS and Filesystem ========
 
     // -------- Metadata --------
diff --git a/crates/cheatcodes/src/lib.rs b/crates/cheatcodes/src/lib.rs
index 95d04cab88e3..732f55d7e253 100644
--- a/crates/cheatcodes/src/lib.rs
+++ b/crates/cheatcodes/src/lib.rs
@@ -37,6 +37,8 @@ mod config;
 
 mod crypto;
 
+mod version;
+
 mod env;
 pub use env::set_execution_context;
 
diff --git a/crates/cheatcodes/src/version.rs b/crates/cheatcodes/src/version.rs
new file mode 100644
index 000000000000..6a5a2793456a
--- /dev/null
+++ b/crates/cheatcodes/src/version.rs
@@ -0,0 +1,51 @@
+use crate::{Cheatcode, Cheatcodes, Result, Vm::*};
+use alloy_sol_types::SolValue;
+use foundry_common::version::SEMVER_VERSION;
+use semver::Version;
+use std::cmp::Ordering;
+
+impl Cheatcode for foundryVersionCmpCall {
+    fn apply(&self, _state: &mut Cheatcodes) -> Result {
+        let Self { version } = self;
+
+        if version.contains("+") || version.contains("-") {
+            return Err(fmt_err!("Version must be in only major.minor.patch format"));
+        }
+
+        let parsed_version = Version::parse(version)
+            .map_err(|e| fmt_err!("Invalid semver format '{}': {}", version, e))?;
+        let current_semver = Version::parse(SEMVER_VERSION)
+            .map_err(|_| fmt_err!("Invalid current version format"))?;
+
+        let current_version =
+            Version::new(current_semver.major, current_semver.minor, current_semver.patch);
+        // Note: returns -1 if current < provided, 0 if equal, 1 if current > provided.
+        let cmp_result = match current_version.cmp(&parsed_version) {
+            Ordering::Less => -1i32,
+            Ordering::Equal => 0i32,
+            Ordering::Greater => 1i32,
+        };
+        Ok(cmp_result.abi_encode())
+    }
+}
+
+impl Cheatcode for foundryVersionAtLeastCall {
+    fn apply(&self, _state: &mut Cheatcodes) -> Result {
+        let Self { version } = self;
+
+        if version.contains("+") || version.contains("-") {
+            return Err(fmt_err!("Version must be in only major.minor.patch format"));
+        }
+
+        let parsed_version =
+            Version::parse(version).map_err(|_| fmt_err!("Invalid version format"))?;
+        let current_semver = Version::parse(SEMVER_VERSION)
+            .map_err(|_| fmt_err!("Invalid current version format"))?;
+
+        let current_version =
+            Version::new(current_semver.major, current_semver.minor, current_semver.patch);
+
+        let at_least = current_version.cmp(&parsed_version) != Ordering::Less;
+        Ok(at_least.abi_encode())
+    }
+}


Test patch:
diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol
index d1a301e0f3df..cc873d3e1be2 100644
--- a/testdata/cheats/Vm.sol
+++ b/testdata/cheats/Vm.sol
@@ -267,6 +267,8 @@ interface Vm {
     function expectSafeMemoryCall(uint64 min, uint64 max) external;
     function fee(uint256 newBasefee) external;
     function ffi(string[] calldata commandInput) external returns (bytes memory result);
+    function foundryVersionAtLeast(string calldata version) external view returns (bool);
+    function foundryVersionCmp(string calldata version) external view returns (int256);
     function fsMetadata(string calldata path) external view returns (FsMetadata memory metadata);
     function getArtifactPathByCode(bytes calldata code) external view returns (string memory path);
     function getArtifactPathByDeployedCode(bytes calldata deployedCode) external view returns (string memory path);
diff --git a/testdata/default/cheats/GetFoundryVersion.t.sol b/testdata/default/cheats/GetFoundryVersion.t.sol
index eca5090db99c..ac30bb849187 100644
--- a/testdata/default/cheats/GetFoundryVersion.t.sol
+++ b/testdata/default/cheats/GetFoundryVersion.t.sol
@@ -42,4 +42,49 @@ contract GetFoundryVersionTest is DSTest {
         // Validate build profile (e.g., "debug" or "release")
         require(bytes(buildType).length > 0, "Build type is empty");
     }
+
+    function testFoundryVersionCmp() public {
+        // Should return -1 if current version is less than argument
+        assertEq(vm.foundryVersionCmp("99.0.0"), -1);
+
+        // (e.g. 0.3.0-nightly+3cb96bde9b.1737036656.debug)
+        string memory fullVersionString = vm.getFoundryVersion();
+
+        // Step 1: Split the version at "+"
+        string[] memory plusSplit = vm.split(fullVersionString, "+");
+        require(plusSplit.length == 2, "Invalid version format: Missing '+' separator");
+
+        // Step 2: Extract parts
+        string memory semanticVersion = plusSplit[0]; // "0.3.0-dev"
+        string[] memory semanticSplit = vm.split(semanticVersion, "-");
+
+        semanticVersion = semanticSplit[0]; // "0.3.0"
+        // Should return 0 if current version is equal to argument
+        assertEq(vm.foundryVersionCmp(semanticVersion), 0);
+
+        // Should return 1 if current version is greater than argument
+        assertEq(vm.foundryVersionCmp("0.0.1"), 1);
+    }
+
+    function testFoundryVersionAtLeast() public {
+        // Should return false for future versions
+        assertEq(vm.foundryVersionAtLeast("99.0.0"), false);
+
+        // (e.g. 0.3.0-nightly+3cb96bde9b.1737036656.debug)
+        string memory fullVersionString = vm.getFoundryVersion();
+
+        // Step 1: Split the version at "+"
+        string[] memory plusSplit = vm.split(fullVersionString, "+");
+        require(plusSplit.length == 2, "Invalid version format: Missing '+' separator");
+
+        // Step 2: Extract parts
+        string memory semanticVersion = plusSplit[0]; // "0.3.0-dev"
+        string[] memory semanticSplit = vm.split(semanticVersion, "-");
+
+        semanticVersion = semanticSplit[0]; // "0.3.0"
+        assertTrue(vm.foundryVersionAtLeast(semanticVersion));
+
+        // Should return true for past versions
+        assertTrue(vm.foundryVersionAtLeast("0.2.0"));
+    }
 }