From 7460d3a01c235ea872a9e47753ae3a5cfecb2577 Mon Sep 17 00:00:00 2001 From: tuturu-tech Date: Mon, 23 Dec 2024 15:43:33 +0100 Subject: [PATCH 01/10] add exploration mode flag --- cmd/fuzz_flags.go | 18 ++++++++++++++++++ docs/src/cli/fuzz.md | 9 +++++++++ 2 files changed, 27 insertions(+) diff --git a/cmd/fuzz_flags.go b/cmd/fuzz_flags.go index 9e1c9d37..d782496f 100644 --- a/cmd/fuzz_flags.go +++ b/cmd/fuzz_flags.go @@ -63,6 +63,9 @@ func addFuzzFlags() error { // Logging color fuzzCmd.Flags().Bool("no-color", false, "disabled colored terminal output") + // Exploration mode + fuzzCmd.Flags().Bool("explore", false, "enables exploration mode") + return nil } @@ -163,5 +166,20 @@ func updateProjectConfigWithFuzzFlags(cmd *cobra.Command, projectConfig *config. return err } } + + // Update configuration to exploration mode + if cmd.Flags().Changed("explore") { + exploreBool, err := cmd.Flags().GetBool("explore") + if err != nil { + return err + } + if exploreBool { + projectConfig.Fuzzing.Testing.StopOnFailedTest = false + projectConfig.Fuzzing.Testing.StopOnNoTests = false + projectConfig.Fuzzing.Testing.AssertionTesting.Enabled = false + projectConfig.Fuzzing.Testing.PropertyTesting.Enabled = false + projectConfig.Fuzzing.Testing.OptimizationTesting.Enabled = false + } + } return nil } diff --git a/docs/src/cli/fuzz.md b/docs/src/cli/fuzz.md index ed70d15a..671723b5 100644 --- a/docs/src/cli/fuzz.md +++ b/docs/src/cli/fuzz.md @@ -129,3 +129,12 @@ The `--no-color` flag disables colored console output (equivalent to # Disable colored output medusa fuzz --no-color ``` + +### `--explore` + +The `--explore` flag enables exploration mode. This sets the [`StopOnFailedTest`](../project_configuration/testing_config.md#stoponfailedtest) and [`StopOnNoTests`](../project_configuration/testing_config.md#stoponnotests) fields to `false` and turns off assertion, property, and optimization testing. + +```shell +# Enable exploration mode +medusa fuzz --explore +``` From dae941783b063b07763a08591e211ff6c1b1e06a Mon Sep 17 00:00:00 2001 From: bohendo Date: Tue, 2 May 2023 12:49:41 -0400 Subject: [PATCH 02/10] init flake.nix --- .gitignore | 6 ++- flake.lock | 42 ++++++++++++++++++ flake.nix | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.gitignore b/.gitignore index b1251402..17d4923a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,14 +13,16 @@ # Dependency directories (remove the comment below to include it) # vendor/ +*node_modules/ # Goland project dir .idea/ -*node_modules/ - # Medusa binary medusa # Medusa docs docs/book + +# Build results +result diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..5a1a2a4b --- /dev/null +++ b/flake.lock @@ -0,0 +1,42 @@ +{ + "nodes": { + "flake-utils": { + "locked": { + "lastModified": 1667395993, + "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1675061157, + "narHash": "sha256-F7/F65ZFWbq7cKSiV3K2acxCv64jKaZZ/K0A3VNT2kA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "f413457e0dd7a42adefdbcea4391dd9751509025", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-22.11", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..281b9069 --- /dev/null +++ b/flake.nix @@ -0,0 +1,128 @@ +{ + description = "Medusa smart-contract fuzzer"; + + inputs = { + nixpkgs.url = "nixpkgs/nixos-22.11"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; config.allowUnfree = true; }; + pyCommon = { + format = "pyproject"; + nativeBuildInputs = with pkgs.python39Packages; [ pythonRelaxDepsHook ]; + pythonRelaxDeps = true; + doCheck = false; + }; + in + rec { + + packages = rec { + + solc-select = pkgs.python39Packages.buildPythonPackage (pyCommon // { + pname = "solc-select"; + version = "1.0.3"; + src = builtins.fetchGit { + url = "git+ssh://git@github.com/crytic/solc-select"; + rev = "97f160611c39d46e27d6f44a5a61344e6218d584"; + }; + propagatedBuildInputs = with pkgs.python39Packages; [ + packaging + setuptools + pycryptodome + ]; + }); + + crytic-compile = pkgs.python39Packages.buildPythonPackage (pyCommon // rec { + pname = "crytic-compile"; + version = "0.3.1"; + src = builtins.fetchGit { + url = "git+ssh://git@github.com/crytic/crytic-compile"; + rev = "10104f33f593ab82ba5780a5fe8dd26385acd1c1"; + }; + propagatedBuildInputs = with pkgs.python39Packages; [ + cbor2 + pycryptodome + setuptools + packages.solc-select + ]; + }); + + slither = pkgs.python39Packages.buildPythonPackage (pyCommon // rec { + pname = "slither"; + version = "0.9.3"; + format = "pyproject"; + src = builtins.fetchGit { + url = "git+ssh://git@github.com/crytic/slither"; + rev = "e6b8af882c6419a9119bec5f4cfff93985a92f4e"; + }; + nativeBuildInputs = with pkgs.python39Packages; [ pythonRelaxDepsHook ]; + pythonRelaxDeps = true; + doCheck = false; + propagatedBuildInputs = with pkgs.python39Packages; [ + packaging + prettytable + pycryptodome + packages.crytic-compile + ]; + postPatch = '' + echo "web3 dependency depends on ipfs which is bugged, removing it from the listed deps" + sed -i 's/"web3>=6.0.0",//' setup.py + ''; + }); + + medusa = pkgs.buildGoModule { + pname = "medusa"; + version = "0.1.0"; # from cmd/root.go + src = ./.; + vendorSha256 = "sha256-odBzty8wgFfdSF18D15jWtUNeQPJ7bkt9k5dx+8EFb4="; + nativeBuildInputs = [ + packages.crytic-compile + pkgs.solc + pkgs.nodejs + ]; + doCheck = false; # tests require `npm install` which can't run in hermetic build env + }; + + default = medusa; + + }; + + apps = { + default = { + type = "app"; + program = "${self.packages.${system}.medusa}/bin/medusa"; + }; + }; + + devShells = { + default = pkgs.mkShell { + buildInputs = with pkgs; [ + packages.medusa + bashInteractive + # runtime dependencies + packages.crytic-compile + packages.slither + solc + # test dependencies + nodejs + # go development + go + gotools + go-tools + gopls + go-outline + gocode + gopkgs + gocode-gomod + godef + golint + ]; + }; + }; + + } + ); +} From faf128b0662915b6ad111d0ad2c5a098029dbd45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20L=C3=B3pez?= Date: Thu, 2 Jan 2025 13:58:06 +0100 Subject: [PATCH 03/10] Update nix flake references --- flake.lock | 32 +++++++++++++++++++++++++------- flake.nix | 46 +++++++++++++++++++++------------------------- 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/flake.lock b/flake.lock index 5a1a2a4b..d853ed8a 100644 --- a/flake.lock +++ b/flake.lock @@ -1,12 +1,15 @@ { "nodes": { "flake-utils": { + "inputs": { + "systems": "systems" + }, "locked": { - "lastModified": 1667395993, - "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -17,16 +20,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1675061157, - "narHash": "sha256-F7/F65ZFWbq7cKSiV3K2acxCv64jKaZZ/K0A3VNT2kA=", + "lastModified": 1735669367, + "narHash": "sha256-tfYRbFhMOnYaM4ippqqid3BaLOXoFNdImrfBfCp4zn0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f413457e0dd7a42adefdbcea4391dd9751509025", + "rev": "edf04b75c13c2ac0e54df5ec5c543e300f76f1c9", "type": "github" }, "original": { "id": "nixpkgs", - "ref": "nixos-22.11", + "ref": "nixos-24.11", "type": "indirect" } }, @@ -35,6 +38,21 @@ "flake-utils": "flake-utils", "nixpkgs": "nixpkgs" } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 281b9069..fd31f4de 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,7 @@ description = "Medusa smart-contract fuzzer"; inputs = { - nixpkgs.url = "nixpkgs/nixos-22.11"; + nixpkgs.url = "nixpkgs/nixos-24.11"; flake-utils.url = "github:numtide/flake-utils"; }; @@ -12,7 +12,7 @@ pkgs = import nixpkgs { inherit system; config.allowUnfree = true; }; pyCommon = { format = "pyproject"; - nativeBuildInputs = with pkgs.python39Packages; [ pythonRelaxDepsHook ]; + nativeBuildInputs = with pkgs.python3Packages; [ pythonRelaxDepsHook ]; pythonRelaxDeps = true; doCheck = false; }; @@ -21,28 +21,28 @@ packages = rec { - solc-select = pkgs.python39Packages.buildPythonPackage (pyCommon // { + solc-select = pkgs.python3Packages.buildPythonPackage (pyCommon // { pname = "solc-select"; - version = "1.0.3"; + version = "1.0.4"; src = builtins.fetchGit { - url = "git+ssh://git@github.com/crytic/solc-select"; - rev = "97f160611c39d46e27d6f44a5a61344e6218d584"; + url = "https://github.com/crytic/solc-select.git"; + rev = "8072a3394bdc960c0f652fb72e928a7eae3631da"; }; - propagatedBuildInputs = with pkgs.python39Packages; [ + propagatedBuildInputs = with pkgs.python3Packages; [ packaging setuptools pycryptodome ]; }); - crytic-compile = pkgs.python39Packages.buildPythonPackage (pyCommon // rec { + crytic-compile = pkgs.python3Packages.buildPythonPackage (pyCommon // rec { pname = "crytic-compile"; - version = "0.3.1"; + version = "0.3.7"; src = builtins.fetchGit { - url = "git+ssh://git@github.com/crytic/crytic-compile"; - rev = "10104f33f593ab82ba5780a5fe8dd26385acd1c1"; + url = "https://github.com/crytic/crytic-compile.git"; + rev = "20df04f37af723eaa7fa56dc2c80169776f3bc4d"; }; - propagatedBuildInputs = with pkgs.python39Packages; [ + propagatedBuildInputs = with pkgs.python3Packages; [ cbor2 pycryptodome setuptools @@ -50,34 +50,31 @@ ]; }); - slither = pkgs.python39Packages.buildPythonPackage (pyCommon // rec { + slither = pkgs.python3Packages.buildPythonPackage (pyCommon // rec { pname = "slither"; - version = "0.9.3"; + version = "0.10.4"; format = "pyproject"; src = builtins.fetchGit { - url = "git+ssh://git@github.com/crytic/slither"; - rev = "e6b8af882c6419a9119bec5f4cfff93985a92f4e"; + url = "https://github.com/crytic/slither.git"; + rev = "aeeb2d368802844733671e35200b30b5f5bdcf5c"; }; - nativeBuildInputs = with pkgs.python39Packages; [ pythonRelaxDepsHook ]; + nativeBuildInputs = with pkgs.python3Packages; [ pythonRelaxDepsHook ]; pythonRelaxDeps = true; doCheck = false; - propagatedBuildInputs = with pkgs.python39Packages; [ + propagatedBuildInputs = with pkgs.python3Packages; [ packaging prettytable pycryptodome packages.crytic-compile + web3 ]; - postPatch = '' - echo "web3 dependency depends on ipfs which is bugged, removing it from the listed deps" - sed -i 's/"web3>=6.0.0",//' setup.py - ''; }); medusa = pkgs.buildGoModule { pname = "medusa"; - version = "0.1.0"; # from cmd/root.go + version = "0.1.8"; # from cmd/root.go src = ./.; - vendorSha256 = "sha256-odBzty8wgFfdSF18D15jWtUNeQPJ7bkt9k5dx+8EFb4="; + vendorHash = "sha256-12Xkg5dzA83HQ2gMngXoLgu1c9KGSL6ly5Qz/o8U++8="; nativeBuildInputs = [ packages.crytic-compile pkgs.solc @@ -114,7 +111,6 @@ go-tools gopls go-outline - gocode gopkgs gocode-gomod godef From b4fa34747984327f1f867918168d77e430fcb67c Mon Sep 17 00:00:00 2001 From: bohendo Date: Mon, 6 Jan 2025 14:23:17 -0500 Subject: [PATCH 04/10] update nix-related documentation --- CONTRIBUTING.md | 10 ++++++++++ README.md | 2 +- docs/src/getting_started/installation.md | 21 +++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dcae4472..98e1a080 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -87,6 +87,16 @@ To run - Ensure JSON keys are `camelCase` rather than `snake_case`, where possible. +### Nix considerations + +- If any dependencies are added or removed, the `vendorHash` property in ./flake.nix will need to be updated. To do so, run `nix build`. If it works, you're good to go. If a change is required, you'll see an error that looks like the following. Replace the `specified` value of `vendorHash` in the medusa package of flake.nix with what nix actually `got`. + +``` +error: hash mismatch in fixed-output derivation '/nix/store/sfgmkr563pzyxzllpmwxdbdxgrav8y1p-medusa-0.1.8-go-modules.drv': + specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + got: sha256-12Xkg5dzA83HQ2gMngXoLgu1c9KGSL6ly5Qz/o8U++8= +``` + ## License The license for this software can be found [here](./LICENSE). diff --git a/README.md b/README.md index 162145df..76f5aa32 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ The master branch can be installed using the following command: brew install --HEAD medusa ``` -For more information on building from source or obtaining binaries for Windows and Linux, please refer to the [installation guide](./docs/src/getting_started/installation.md). +For more information on building from source, using nix, or obtaining binaries for Windows and Linux, please refer to the [installation guide](./docs/src/getting_started/installation.md). ## Contributing diff --git a/docs/src/getting_started/installation.md b/docs/src/getting_started/installation.md index 531758ec..1bdea5db 100644 --- a/docs/src/getting_started/installation.md +++ b/docs/src/getting_started/installation.md @@ -22,6 +22,27 @@ Run the following command to install `medusa`: brew install medusa ``` +## Installing with Nix + +### Prerequisites + +Make sure nix is installed and that `nix-command` and `flake` features are enabled. The [Determinate Systems nix-installer](https://determinate.systems/nix-installer/) will automatically enable these features and is the recommended approach. If nix is already installed without these features enabled, run the following commands. + +``` +mkdir -p ~/.config/nix +echo 'experimental-features = nix-command flakes' > ~/.config/nix/nix.conf +``` + +### Build `medusa` + +`nix build` will build medusa and wire up independent copies of required dependencies. The resulting binary can be found at `./result/bin/medusa` + +### Install `medusa` + +After building, you can add the build result to your PATH using nix profiles by running the following command: + +`nix profile install ./result` + ## Building from source ### Prerequisites From 350ee4badd95135970c8d1b23bfa47be4fd8c63a Mon Sep 17 00:00:00 2001 From: tuturu-tech <9058533+tuturu-tech@users.noreply.github.com> Date: Mon, 6 Jan 2025 22:24:48 +0100 Subject: [PATCH 05/10] Dev/fail fast cli flag (#525) * add fail fast flag * fix docs typo * minor changes --------- Co-authored-by: anishnaik --- cmd/fuzz_flags.go | 18 +++++++++++++++--- docs/src/cli/fuzz.md | 10 ++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/cmd/fuzz_flags.go b/cmd/fuzz_flags.go index d782496f..56d17897 100644 --- a/cmd/fuzz_flags.go +++ b/cmd/fuzz_flags.go @@ -61,7 +61,10 @@ func addFuzzFlags() error { fmt.Sprintf("print the execution trace for every element in a shrunken call sequence instead of only the last element (unless a config file is provided, default is %t)", defaultConfig.Fuzzing.Testing.TraceAll)) // Logging color - fuzzCmd.Flags().Bool("no-color", false, "disabled colored terminal output") + fuzzCmd.Flags().Bool("no-color", false, "disables colored terminal output") + + // Enable stop on failed test + fuzzCmd.Flags().Bool("fail-fast", false, "enables stop on failed test") // Exploration mode fuzzCmd.Flags().Bool("explore", false, "enables exploration mode") @@ -167,13 +170,22 @@ func updateProjectConfigWithFuzzFlags(cmd *cobra.Command, projectConfig *config. } } + // Update stop on failed test feature + if cmd.Flags().Changed("fail-fast") { + failFast, err := cmd.Flags().GetBool("fail-fast") + if err != nil { + return err + } + projectConfig.Fuzzing.Testing.StopOnFailedTest = failFast + } + // Update configuration to exploration mode if cmd.Flags().Changed("explore") { - exploreBool, err := cmd.Flags().GetBool("explore") + explore, err := cmd.Flags().GetBool("explore") if err != nil { return err } - if exploreBool { + if explore { projectConfig.Fuzzing.Testing.StopOnFailedTest = false projectConfig.Fuzzing.Testing.StopOnNoTests = false projectConfig.Fuzzing.Testing.AssertionTesting.Enabled = false diff --git a/docs/src/cli/fuzz.md b/docs/src/cli/fuzz.md index 671723b5..6430b3ed 100644 --- a/docs/src/cli/fuzz.md +++ b/docs/src/cli/fuzz.md @@ -109,6 +109,16 @@ The `--deployer` flag allows you to update `medusa`'s contract deployer (equival medusa fuzz --deployer "0x40000" ``` +### `--fail-fast` + +The `--fail-fast` flag enables fast failure (equivalent to +[`testing.StopOnFailedTest`](../project_configuration/testing_config.md#stoponfailedtest)) + +```shell +# Enable fast failure +medusa fuzz --fail-fast +``` + ### `--trace-all` The `--trace-all` flag allows you to retrieve an execution trace for each element of a call sequence that triggered a test From 0112ddc7a540d973b611def13c2db364d5a8d491 Mon Sep 17 00:00:00 2001 From: anishnaik Date: Wed, 8 Jan 2025 18:39:49 -0500 Subject: [PATCH 06/10] Add slither integration (#530) * Initial Slither integration * add slither config and basic test * add caching * complete unit test and improve logging * add newline * update CI and allow for caching to disk * fix linter * fix linting again * add --use-slither flag to run slither on the fly * prevent other fuzz tests from using slither to speed up CI * update documentation for slither integration * throw error before logging * fix comment formatting. * linting is very annoying * improve error handling * improve documentation * remove -1 from value set when adding bools * fix test --------- Co-authored-by: Simone --- .github/workflows/ci.yml | 2 +- cmd/fuzz_flags.go | 14 ++ compilation/types/slither.go | 203 ++++++++++++++++++ docs/src/project_configuration/overview.md | 2 + .../project_configuration/slither_config.md | 27 +++ fuzzing/config/config.go | 4 + fuzzing/config/config_defaults.go | 8 + fuzzing/fuzzer.go | 38 +++- fuzzing/fuzzer_test.go | 71 ++++++ .../testdata/contracts/slither/slither.sol | 18 ++ .../valuegeneration/value_set_from_slither.go | 39 ++++ 11 files changed, 422 insertions(+), 4 deletions(-) create mode 100644 compilation/types/slither.go create mode 100644 docs/src/project_configuration/slither_config.md create mode 100644 fuzzing/testdata/contracts/slither/slither.sol create mode 100644 fuzzing/valuegeneration/value_set_from_slither.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6983fbc2..88b63890 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -214,7 +214,7 @@ jobs: - name: Install Python dependencies run: | - pip3 install --no-cache-dir solc-select crytic-compile + pip3 install --no-cache-dir solc-select slither-analyzer - name: Install solc run: | diff --git a/cmd/fuzz_flags.go b/cmd/fuzz_flags.go index 56d17897..7a07360d 100644 --- a/cmd/fuzz_flags.go +++ b/cmd/fuzz_flags.go @@ -69,6 +69,8 @@ func addFuzzFlags() error { // Exploration mode fuzzCmd.Flags().Bool("explore", false, "enables exploration mode") + // Run slither on-the-fly + fuzzCmd.Flags().Bool("use-slither", false, "runs slither") return nil } @@ -193,5 +195,17 @@ func updateProjectConfigWithFuzzFlags(cmd *cobra.Command, projectConfig *config. projectConfig.Fuzzing.Testing.OptimizationTesting.Enabled = false } } + + // Update configuration to run slither + if cmd.Flags().Changed("use-slither") { + useSlither, err := cmd.Flags().GetBool("use-slither") + if err != nil { + return err + } + if useSlither { + projectConfig.Slither.UseSlither = true + } + } + return nil } diff --git a/compilation/types/slither.go b/compilation/types/slither.go new file mode 100644 index 00000000..3a31d9ff --- /dev/null +++ b/compilation/types/slither.go @@ -0,0 +1,203 @@ +package types + +import ( + "encoding/json" + "errors" + "github.com/crytic/medusa/logging" + "os" + "os/exec" + "time" +) + +// SlitherConfig determines whether to run slither and whether and where to cache the results from slither +type SlitherConfig struct { + // UseSlither determines whether to use slither. If CachePath is non-empty, then the cached results will be + // attempted to be used. Otherwise, slither will be run. + UseSlither bool `json:"useSlither"` + // CachePath determines the path where the slither cache file will be located + CachePath string `json:"cachePath"` +} + +// NewDefaultSlitherConfig provides a default configuration to run slither. The default configuration enables the +// running of slither with the use of a cache. +func NewDefaultSlitherConfig() (*SlitherConfig, error) { + return &SlitherConfig{ + UseSlither: true, + CachePath: "slither_results.json", + }, nil +} + +// SlitherResults describes a data structures that holds the interesting constants returned from slither +type SlitherResults struct { + // Constants holds the constants extracted by slither + Constants []Constant `json:"constantsUsed"` +} + +// Constant defines a constant that was extracted by slither while parsing the compilation target +type Constant struct { + // Type represents the ABI type of the constant + Type string `json:"type"` + // Value represents the value of the constant + Value string `json:"value"` +} + +// RunSlither on the provided compilation target. RunSlither will use cached results if they exist and write to the +// cache if we have not written to the cache already. A SlitherResults data structure is returned. +func (s *SlitherConfig) RunSlither(target string) (*SlitherResults, error) { + // Return early if we do not want to run slither + if !s.UseSlither { + return nil, nil + } + + // Use the cached slither output if it exists + var haveCachedResults bool + var out []byte + var err error + if s.CachePath != "" { + // Check to see if the file exists in the first place. + // If not, we will re-run slither + if _, err = os.Stat(s.CachePath); os.IsNotExist(err) { + logging.GlobalLogger.Info("No Slither cached results found at ", s.CachePath) + haveCachedResults = false + } else { + // We found the cached file + if out, err = os.ReadFile(s.CachePath); err != nil { + return nil, err + } + haveCachedResults = true + logging.GlobalLogger.Info("Using cached Slither results found at ", s.CachePath) + } + } + + // Run slither if we do not have cached results, or we cannot find the cached results + if !haveCachedResults { + // Log the command + cmd := exec.Command("slither", target, "--ignore-compile", "--print", "echidna", "--json", "-") + logging.GlobalLogger.Info("Running Slither:\n", cmd.String()) + + // Run slither + start := time.Now() + out, err = cmd.CombinedOutput() + if err != nil { + return nil, err + } + logging.GlobalLogger.Info("Finished running Slither in ", time.Since(start).Round(time.Second)) + } + + // Capture the slither results + var slitherResults SlitherResults + err = json.Unmarshal(out, &slitherResults) + if err != nil { + return nil, err + } + + // Cache the results if we have not cached before. We have also already checked that the output is well-formed + // (through unmarshal) so we should be safe. + if !haveCachedResults && s.CachePath != "" { + // Cache the data + err = os.WriteFile(s.CachePath, out, 0644) + if err != nil { + // If we are unable to write to the cache, we should log the error but continue + logging.GlobalLogger.Warn("Failed to cache Slither results at ", s.CachePath, " due to an error:", err) + // It is possible for os.WriteFile to create a partially written file so it is best to try to delete it + if _, err = os.Stat(s.CachePath); err == nil { + // We will not handle the error of os.Remove since we have already checked for the file's existence + // and we have the right permissions. + os.Remove(s.CachePath) + } + } + } + + return &slitherResults, nil +} + +// UnmarshalJSON unmarshals the slither output into a Slither type +func (s *SlitherResults) UnmarshalJSON(d []byte) error { + // Extract the top-level JSON object + var obj map[string]json.RawMessage + if err := json.Unmarshal(d, &obj); err != nil { + return err + } + + // Decode success and error. They are always present in the slither output + var success bool + var slitherError string + if err := json.Unmarshal(obj["success"], &success); err != nil { + return err + } + + if err := json.Unmarshal(obj["error"], &slitherError); err != nil { + return err + } + + // If success is not true or there is a non-empty error string, return early + if !success || slitherError != "" { + if slitherError != "" { + return errors.New(slitherError) + } + return errors.New("slither returned a failure during parsing") + } + + // Now we will extract the constants + s.Constants = make([]Constant, 0) + + // Iterate through the JSON object until we get to the constants_used key + // First, retrieve the results + var results map[string]json.RawMessage + if err := json.Unmarshal(obj["results"], &results); err != nil { + return err + } + + // Retrieve the printers data + var printers []json.RawMessage + if err := json.Unmarshal(results["printers"], &printers); err != nil { + return err + } + + // Since we are running the echidna printer, we know that the first element is the one we care about + var echidnaPrinter map[string]json.RawMessage + if err := json.Unmarshal(printers[0], &echidnaPrinter); err != nil { + return err + } + + // We need to de-serialize the description in two separate steps because go is dumb sometimes + var descriptionString string + if err := json.Unmarshal(echidnaPrinter["description"], &descriptionString); err != nil { + return err + } + var description map[string]json.RawMessage + if err := json.Unmarshal([]byte(descriptionString), &description); err != nil { + return err + } + + // Capture all the constants extracted across all the contracts in scope + var constantsInContracts map[string]json.RawMessage + if err := json.Unmarshal(description["constants_used"], &constantsInContracts); err != nil { + return err + } + + // Iterate across the constants in each contract + for _, constantsInContract := range constantsInContracts { + // Capture all the constants in a given function + var constantsInFunctions map[string]json.RawMessage + if err := json.Unmarshal(constantsInContract, &constantsInFunctions); err != nil { + return err + } + + // Iterate across each function + for _, constantsInFunction := range constantsInFunctions { + // Each constant is provided as its own list, so we need to create a matrix + var constants [][]Constant + if err := json.Unmarshal(constantsInFunction, &constants); err != nil { + return err + } + for _, constant := range constants { + // Slither outputs the value of a constant as a list + // However we know there can be only 1 so we take index 0 + s.Constants = append(s.Constants, constant[0]) + } + } + } + + return nil +} diff --git a/docs/src/project_configuration/overview.md b/docs/src/project_configuration/overview.md index 972bd0c7..a9078a3b 100644 --- a/docs/src/project_configuration/overview.md +++ b/docs/src/project_configuration/overview.md @@ -7,6 +7,8 @@ configuration is a `.json` file that is broken down into five core components. - [Testing Configuration](./testing_config.md): The testing configuration dictates how and what `medusa` should fuzz test. - [Chain Configuration](./chain_config.md): The chain configuration dictates how `medusa`'s underlying blockchain should be configured. - [Compilation Configuration](./compilation_config.md): The compilation configuration dictates how to compile the fuzzing target. +- [Slither Configuration](./slither_config.md): The Slither configuration dictates whether Slither should be used in + `medusa` and whether the results from Slither should be cached. - [Logging Configuration](./logging_config.md): The logging configuration dictates when and where to log events. To generate a project configuration file, run [`medusa init`](../cli/init.md). diff --git a/docs/src/project_configuration/slither_config.md b/docs/src/project_configuration/slither_config.md new file mode 100644 index 00000000..b7959d3f --- /dev/null +++ b/docs/src/project_configuration/slither_config.md @@ -0,0 +1,27 @@ +# Slither Configuration + +The [Slither](https://github.com/crytic/slither) configuration defines the parameters for using Slither in `medusa`. +Currently, we use Slither to extract interesting constants from the target system. These constants are then used in the +fuzzing process to try to increase coverage. Note that if Slither fails to run for some reason, we will still try our +best to mine constants from each contract's AST so don't worry! + +- > 🚩 We _highly_ recommend using Slither and caching the results. Basically, don't change this configuration unless + > absolutely necessary. The constants identified by Slither are shown to greatly improve system coverage and caching + > the results will improve the speed of medusa. + +### `useSlither` + +- **Type**: Boolean +- **Description**: If `true`, Slither will be run on the target system and useful constants will be extracted for fuzzing. + If `cachePath` is a non-empty string (which it is by default), then `medusa` will first check the cache before running + Slither. +- **Default**: `true` + +### `cachePath` + +- **Type**: String +- **Description**: If `cachePath` is non-empty, Slither's results will be cached on disk. When `medusa` is re-run, these + cached results will be used. We do this for performance reasons since re-running Slither each time `medusa` is restarted + is computationally intensive for complex projects. We recommend disabling caching (by making `cachePath` an empty string) + if the target codebase changes. If the code remains constant during the fuzzing campaign, we recommend to use the cache. +- **Default**: `slither_results.json` diff --git a/fuzzing/config/config.go b/fuzzing/config/config.go index 1ccbfb61..bafec51b 100644 --- a/fuzzing/config/config.go +++ b/fuzzing/config/config.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/crytic/medusa/compilation/types" "math/big" "os" @@ -27,6 +28,9 @@ type ProjectConfig struct { // Compilation describes the configuration used to compile the underlying project. Compilation *compilation.CompilationConfig `json:"compilation"` + // Slither describes the configuration for running slither + Slither *types.SlitherConfig `json:"slither"` + // Logging describes the configuration used for logging to file and console Logging LoggingConfig `json:"logging"` } diff --git a/fuzzing/config/config_defaults.go b/fuzzing/config/config_defaults.go index 38532a03..af278c90 100644 --- a/fuzzing/config/config_defaults.go +++ b/fuzzing/config/config_defaults.go @@ -1,6 +1,7 @@ package config import ( + "github.com/crytic/medusa/compilation/types" "math/big" testChainConfig "github.com/crytic/medusa/chain/config" @@ -31,6 +32,12 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) { return nil, err } + // Obtain a default slither configuration + slitherConfig, err := types.NewDefaultSlitherConfig() + if err != nil { + return nil, err + } + // Create a project configuration projectConfig := &ProjectConfig{ Fuzzing: FuzzingConfig{ @@ -88,6 +95,7 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) { TestChainConfig: *chainConfig, }, Compilation: compilationConfig, + Slither: slitherConfig, Logging: LoggingConfig{ Level: zerolog.InfoLevel, LogDirectory: "", diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index 4d862dc0..9e95fca1 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -60,6 +60,10 @@ type Fuzzer struct { // contractDefinitions defines targets to be fuzzed once their deployment is detected. They are derived from // compilations. contractDefinitions fuzzerTypes.Contracts + // slitherResults holds the results obtained from slither. At the moment we do not have use for storing this in the + // Fuzzer but down the line we can use slither for other capabilities that may require storage of the results. + slitherResults *compilationTypes.SlitherResults + // baseValueSet represents a valuegeneration.ValueSet containing input values for our fuzz tests. baseValueSet *valuegeneration.ValueSet @@ -284,7 +288,32 @@ func (f *Fuzzer) ReportTestCaseFinished(testCase TestCase) { // AddCompilationTargets takes a compilation and updates the Fuzzer state with additional Fuzzer.ContractDefinitions // definitions and Fuzzer.BaseValueSet values. func (f *Fuzzer) AddCompilationTargets(compilations []compilationTypes.Compilation) { - // Loop for each contract in each compilation and deploy it to the test chain + var seedFromAST bool + + // No need to handle the error here since having compilation artifacts implies that we used a supported + // platform configuration + platformConfig, _ := f.config.Compilation.GetPlatformConfig() + + // Retrieve the compilation target for slither + target := platformConfig.GetTarget() + + // Run slither and handle errors + slitherResults, err := f.config.Slither.RunSlither(target) + if err != nil || slitherResults == nil { + if err != nil { + f.logger.Warn("Failed to run slither", err) + } + seedFromAST = true + } + + // If we have results and there were no errors, we will seed the value set using the slither results + if !seedFromAST { + f.slitherResults = slitherResults + // Seed our base value set with the constants extracted by Slither + f.baseValueSet.SeedFromSlither(slitherResults) + } + + // Capture all the contract definitions, functions, and cache the source code for i := 0; i < len(compilations); i++ { // Add our compilation to the list and get a reference to it. f.compilations = append(f.compilations, compilations[i]) @@ -292,8 +321,11 @@ func (f *Fuzzer) AddCompilationTargets(compilations []compilationTypes.Compilati // Loop for each source for sourcePath, source := range compilation.SourcePathToArtifact { - // Seed our base value set from every source's AST - f.baseValueSet.SeedFromAst(source.Ast) + // Seed from the contract's AST if we did not use slither or failed to do so + if seedFromAST { + // Seed our base value set from every source's AST + f.baseValueSet.SeedFromAst(source.Ast) + } // Loop for every contract and register it in our contract definitions for contractName := range source.Contracts { diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 06f0992b..aa25bcb4 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -28,6 +28,7 @@ func TestFuzzerHooks(t *testing.T) { config.Fuzzing.TargetContracts = []string{"TestContract"} config.Fuzzing.Testing.PropertyTesting.Enabled = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Attach to fuzzer hooks which simply set a success state. @@ -62,6 +63,51 @@ func TestFuzzerHooks(t *testing.T) { }) } +// TestSlitherPrinter runs slither and ensures that the constants are correctly added to the value set +func TestSlitherPrinter(t *testing.T) { + expectedInts := []int64{ + 123, // value of `x` + 12, // constant in testFuzz + 135, // sum of 123 + 12 + 456, // value of `y` + -123, // negative of 123 + -12, // negative of 12 + -135, // negative of 135 + -456, // negative of 456 + 0, // the false in testFuzz is added as zero in the value set + 1, // true is evaluated as 1 + } + expectedAddrs := []common.Address{ + common.HexToAddress("0"), + } + expectedStrings := []string{ + "Hello World!", + } + // We actually don't need to start the fuzzer and only care about the instantiation of the fuzzer + runFuzzerTest(t, &fuzzerSolcFileTest{ + filePath: "testdata/contracts/slither/slither.sol", + configUpdates: func(config *config.ProjectConfig) { + config.Fuzzing.TargetContracts = []string{"TestContract"} + }, + method: func(f *fuzzerTestContext) { + // Look through the value set to make sure all the ints, addrs, and strings are in there + + // Check for ints + for _, x := range expectedInts { + assert.True(t, f.fuzzer.baseValueSet.ContainsInteger(new(big.Int).SetInt64(x))) + } + // Check for addresses + for _, addr := range expectedAddrs { + assert.True(t, f.fuzzer.baseValueSet.ContainsAddress(addr)) + } + // Check for strings + for _, str := range expectedStrings { + assert.True(t, f.fuzzer.baseValueSet.ContainsString(str)) + } + }, + }) +} + // TestAssertionMode runs tests to ensure that assertion testing behaves as expected. func TestAssertionMode(t *testing.T) { filePaths := []string{ @@ -94,6 +140,7 @@ func TestAssertionMode(t *testing.T) { config.Fuzzing.Testing.AssertionTesting.TestViewMethods = true config.Fuzzing.Testing.PropertyTesting.Enabled = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -117,6 +164,7 @@ func TestAssertionsNotRequire(t *testing.T) { config.Fuzzing.TestLimit = 500 config.Fuzzing.Testing.PropertyTesting.Enabled = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -139,6 +187,7 @@ func TestAssertionsAndProperties(t *testing.T) { config.Fuzzing.TestLimit = 500 config.Fuzzing.Testing.StopOnFailedTest = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -164,6 +213,7 @@ func TestOptimizationMode(t *testing.T) { config.Fuzzing.TestLimit = 10_000 // this test should expose a failure quickly. config.Fuzzing.Testing.PropertyTesting.Enabled = false config.Fuzzing.Testing.AssertionTesting.Enabled = false + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -195,6 +245,7 @@ func TestChainBehaviour(t *testing.T) { config.Fuzzing.TransactionGasLimit = 500000 // we set this low, so contract execution runs out of gas earlier. config.Fuzzing.Testing.AssertionTesting.Enabled = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -256,6 +307,7 @@ func TestCheatCodes(t *testing.T) { config.Fuzzing.TestChainConfig.CheatCodeConfig.CheatCodesEnabled = true config.Fuzzing.TestChainConfig.CheatCodeConfig.EnableFFI = true + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -292,6 +344,7 @@ func TestConsoleLog(t *testing.T) { config.Fuzzing.TestLimit = 10000 config.Fuzzing.Testing.PropertyTesting.Enabled = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -340,6 +393,7 @@ func TestDeploymentsInnerDeployments(t *testing.T) { config.Fuzzing.Testing.TestAllContracts = true // test dynamically deployed contracts config.Fuzzing.Testing.AssertionTesting.Enabled = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -363,6 +417,7 @@ func TestDeploymentsInnerDeployments(t *testing.T) { config.Fuzzing.Testing.TestAllContracts = true // test dynamically deployed contracts config.Fuzzing.Testing.AssertionTesting.Enabled = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -384,6 +439,7 @@ func TestDeploymentsInternalLibrary(t *testing.T) { config.Fuzzing.TestLimit = 100 // this test should expose a failure quickly. config.Fuzzing.Testing.AssertionTesting.Enabled = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -408,6 +464,7 @@ func TestDeploymentsWithPredeploy(t *testing.T) { config.Fuzzing.Testing.PropertyTesting.Enabled = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false config.Fuzzing.PredeployedContracts = map[string]string{"PredeployContract": "0x1234"} + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -431,6 +488,7 @@ func TestDeploymentsWithPayableConstructors(t *testing.T) { config.Fuzzing.TestLimit = 1 // this should happen immediately config.Fuzzing.Testing.AssertionTesting.Enabled = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -462,6 +520,7 @@ func TestDeploymentsSelfDestruct(t *testing.T) { config.Fuzzing.Testing.AssertionTesting.Enabled = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false config.Fuzzing.Testing.TestAllContracts = true + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Subscribe to any mined block events globally. When receiving them, check contract changes for a @@ -508,6 +567,7 @@ func TestExecutionTraces(t *testing.T) { config.Fuzzing.TargetContracts = []string{"TestContract"} config.Fuzzing.Testing.PropertyTesting.Enabled = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -550,6 +610,7 @@ func TestTestingScope(t *testing.T) { config.Fuzzing.Testing.TestAllContracts = testingAllContracts config.Fuzzing.Testing.StopOnFailedTest = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -597,6 +658,7 @@ func TestDeploymentsWithArgs(t *testing.T) { config.Fuzzing.TestLimit = 500 // this test should expose a failure quickly. config.Fuzzing.Testing.AssertionTesting.Enabled = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -618,6 +680,7 @@ func TestValueGenerationGenerateAllTypes(t *testing.T) { config.Fuzzing.TestLimit = 10_000 config.Fuzzing.Testing.AssertionTesting.Enabled = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -652,6 +715,7 @@ func TestValueGenerationSolving(t *testing.T) { config.Fuzzing.TargetContracts = []string{"TestContract"} config.Fuzzing.Testing.AssertionTesting.Enabled = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -710,6 +774,7 @@ func TestASTValueExtraction(t *testing.T) { config.Fuzzing.Testing.PropertyTesting.Enabled = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false config.Fuzzing.TargetContracts = []string{"TestContract"} + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -748,6 +813,7 @@ func TestVMCorrectness(t *testing.T) { config.Fuzzing.MaxBlockNumberDelay = 1 // this contract require calls every block config.Fuzzing.Testing.AssertionTesting.Enabled = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -787,6 +853,7 @@ func TestVMCorrectness(t *testing.T) { config.Fuzzing.TestLimit = 1_000 // this test should expose a failure quickly. config.Fuzzing.MaxBlockTimestampDelay = 1 // this contract require calls every block config.Fuzzing.MaxBlockNumberDelay = 1 // this contract require calls every block + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -812,6 +879,7 @@ func TestCorpusReplayability(t *testing.T) { config.Fuzzing.CorpusDirectory = "corpus" config.Fuzzing.Testing.AssertionTesting.Enabled = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Setup checks for event emissions @@ -868,6 +936,7 @@ func TestDeploymentOrderWithCoverage(t *testing.T) { config.Fuzzing.TargetContracts = []string{"InheritedFirstContract", "InheritedSecondContract"} config.Fuzzing.Testing.AssertionTesting.Enabled = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { // Setup checks for event emissions @@ -913,6 +982,7 @@ func TestTargetingFuncSignatures(t *testing.T) { configUpdates: func(config *config.ProjectConfig) { config.Fuzzing.TargetContracts = []string{"TestContract"} config.Fuzzing.Testing.TargetFunctionSignatures = targets + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { for _, contract := range f.fuzzer.ContractDefinitions() { @@ -934,6 +1004,7 @@ func TestExcludeFunctionSignatures(t *testing.T) { configUpdates: func(config *config.ProjectConfig) { config.Fuzzing.TargetContracts = []string{"TestContract"} config.Fuzzing.Testing.ExcludeFunctionSignatures = excluded + config.Slither.UseSlither = false }, method: func(f *fuzzerTestContext) { for _, contract := range f.fuzzer.ContractDefinitions() { diff --git a/fuzzing/testdata/contracts/slither/slither.sol b/fuzzing/testdata/contracts/slither/slither.sol new file mode 100644 index 00000000..76a4fc06 --- /dev/null +++ b/fuzzing/testdata/contracts/slither/slither.sol @@ -0,0 +1,18 @@ +// This test ensures that all the constants in this contract are added to the value set +contract TestContract { + // Uint constant + uint256 constant x = 123 + 12; + + constructor() {} + + function testFuzz(uint256 z) public { + // Set z to x so that 123, 12, and (123 + 12 = 135) are captured as constants + z = x; + // Add a bunch of other constants + int256 y = 456; + address addr = address(0); + bool b = true; + string memory str = "Hello World!"; + assert(false); + } +} diff --git a/fuzzing/valuegeneration/value_set_from_slither.go b/fuzzing/valuegeneration/value_set_from_slither.go new file mode 100644 index 00000000..dd5f8331 --- /dev/null +++ b/fuzzing/valuegeneration/value_set_from_slither.go @@ -0,0 +1,39 @@ +package valuegeneration + +import ( + "math/big" + "strings" + + compilationTypes "github.com/crytic/medusa/compilation/types" + "github.com/ethereum/go-ethereum/common" +) + +// SeedFromSlither allows a ValueSet to be seeded from the output of slither. +func (vs *ValueSet) SeedFromSlither(slither *compilationTypes.SlitherResults) { + // Iterate across all the constants + for _, constant := range slither.Constants { + // Capture uint/int types + if strings.HasPrefix(constant.Type, "uint") || strings.HasPrefix(constant.Type, "int") { + var b, _ = new(big.Int).SetString(constant.Value, 10) + vs.AddInteger(b) + vs.AddInteger(new(big.Int).Neg(b)) + vs.AddBytes(b.Bytes()) + } else if constant.Type == "bool" { + // Capture booleans + if constant.Value == "False" { + vs.AddInteger(big.NewInt(0)) + } else { + vs.AddInteger(big.NewInt(1)) + } + } else if constant.Type == "string" { + // Capture strings + vs.AddString(constant.Value) + vs.AddBytes([]byte(constant.Value)) + } else if constant.Type == "address" { + // Capture addresses + var addressBigInt, _ = new(big.Int).SetString(constant.Value, 10) + vs.AddAddress(common.BigToAddress(addressBigInt)) + vs.AddBytes([]byte(constant.Value)) + } + } +} From 11d0ac5502d5bb796477d6420ab9f9b28146e436 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Thu, 9 Jan 2025 08:42:46 -0600 Subject: [PATCH 07/10] fix: dedup pc by codehash for unique pc metric (#485) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: dedup pc by codehash for unique pc metric * fix: unify coverage across multiple instances of a codehash when counting unique PCs --------- Co-authored-by: Emilio López Co-authored-by: anishnaik --- fuzzing/coverage/coverage_maps.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/fuzzing/coverage/coverage_maps.go b/fuzzing/coverage/coverage_maps.go index 59237669..2d8c0b0f 100644 --- a/fuzzing/coverage/coverage_maps.go +++ b/fuzzing/coverage/coverage_maps.go @@ -3,11 +3,12 @@ package coverage import ( "golang.org/x/exp/slices" + "sync" + compilationTypes "github.com/crytic/medusa/compilation/types" "github.com/crytic/medusa/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" - "sync" ) // CoverageMaps represents a data structure used to identify instruction execution coverage of various smart contracts @@ -249,6 +250,10 @@ func (cm *CoverageMaps) UniquePCs() uint64 { uniquePCs := uint64(0) // Iterate across each contract deployment for _, mapsByAddress := range cm.maps { + // Consider the coverage of all of the different deployments of this codehash as a set + // And mark a PC as hit if any of the instances has a hit for it + uniquePCsForHash := make(map[int]struct{}) + for _, contractCoverageMap := range mapsByAddress { // TODO: Note we are not checking for nil dereference here because we are guaranteed that the successful // coverage and reverted coverage arrays have been instantiated if we are iterating over it @@ -259,7 +264,7 @@ func (cm *CoverageMaps) UniquePCs() uint64 { for i, hits := range contractCoverageMap.successfulCoverage.executedFlags { // If we hit the PC at least once, we have a unique PC hit if hits != 0 { - uniquePCs++ + uniquePCsForHash[i] = struct{}{} // Do not count both success and revert continue @@ -267,10 +272,12 @@ func (cm *CoverageMaps) UniquePCs() uint64 { // This is only executed if the PC was not executed successfully if contractCoverageMap.revertedCoverage.executedFlags != nil && contractCoverageMap.revertedCoverage.executedFlags[i] != 0 { - uniquePCs++ + uniquePCsForHash[i] = struct{}{} } } } + + uniquePCs += uint64(len(uniquePCsForHash)) } return uniquePCs } From 564204684f11a7d37ad28f3014cec36326ccbc76 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Thu, 9 Jan 2025 09:10:05 -0600 Subject: [PATCH 08/10] fix: weight corpus by timestamp to favor 'hardest-to-discover' inputs (#383) * fix: weight corpus by timestamp to favor 'hardest-to-discover' inputs * use first match * fix error handling --------- Co-authored-by: Anish Naik --- fuzzing/corpus/corpus.go | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/fuzzing/corpus/corpus.go b/fuzzing/corpus/corpus.go index 8a640298..2f820154 100644 --- a/fuzzing/corpus/corpus.go +++ b/fuzzing/corpus/corpus.go @@ -3,22 +3,22 @@ package corpus import ( "bytes" "fmt" - "math/big" - "os" - "path/filepath" - "sync" - "time" - - "github.com/crytic/medusa/utils" - "github.com/crytic/medusa/chain" "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/coverage" "github.com/crytic/medusa/logging" "github.com/crytic/medusa/logging/colors" + "github.com/crytic/medusa/utils" "github.com/crytic/medusa/utils/randomutils" "github.com/ethereum/go-ethereum/common" "github.com/google/uuid" + "math/big" + "os" + "path/filepath" + "regexp" + "strconv" + "sync" + "time" "github.com/crytic/medusa/fuzzing/contracts" ) @@ -282,7 +282,22 @@ func (c *Corpus) initializeSequences(sequenceFiles *corpusDirectory[calls.CallSe // If the sequence was replayed successfully, we add it. If it was not, we exclude it with a warning. if sequenceInvalidError == nil { if useInMutations && c.mutationTargetSequenceChooser != nil { - c.mutationTargetSequenceChooser.AddChoices(randomutils.NewWeightedRandomChoice[calls.CallSequence](sequence, big.NewInt(1))) + // If the filename is a timestamp as expected, use it as a weight for the mutation chooser. + re := regexp.MustCompile("[0-9]+") + var weight *big.Int + if filename := re.FindAllString(sequenceFileData.fileName, 1); len(filename) > 0 { + // The timestamp will be the only element in the filename array + // If we can parse the timestamp with no errors, set the weight + if timestamp, err := strconv.ParseUint(filename[0], 10, 64); err == nil { + weight = new(big.Int).SetUint64(timestamp) + } + } + + // Fallback to 1 if we couldn't parse the timestamp. + if weight == nil { + weight = big.NewInt(1) + } + c.mutationTargetSequenceChooser.AddChoices(randomutils.NewWeightedRandomChoice[calls.CallSequence](sequence, weight)) } c.unexecutedCallSequences = append(c.unexecutedCallSequences, sequence) } else { From bf9430cbcc30f0fbaa04394e41724f0b1341d758 Mon Sep 17 00:00:00 2001 From: Hex <165055168+hexshire@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:39:18 -0300 Subject: [PATCH 09/10] Update solidity_errors.go (#536) --- compilation/abiutils/solidity_errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compilation/abiutils/solidity_errors.go b/compilation/abiutils/solidity_errors.go index 88ce7163..98a1171f 100644 --- a/compilation/abiutils/solidity_errors.go +++ b/compilation/abiutils/solidity_errors.go @@ -119,7 +119,7 @@ func GetPanicReason(panicCode uint64) string { case PanicCodeAssertFailed: return "panic: assertion failed" case PanicCodeArithmeticUnderOverflow: - return "panic: arithmetic underflow" + return "panic: arithmetic underflow/overflow" case PanicCodeDivideByZero: return "panic: division by zero" case PanicCodeEnumTypeConversionOutOfBounds: From 68a14ff71439821e69c0ea427476dddd3e174523 Mon Sep 17 00:00:00 2001 From: Mukul Kolpe Date: Tue, 14 Jan 2025 22:53:16 +0530 Subject: [PATCH 10/10] fix: Return correct error in Corpus.Initialize (#537) Co-authored-by: anishnaik --- fuzzing/corpus/corpus.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/fuzzing/corpus/corpus.go b/fuzzing/corpus/corpus.go index 2f820154..8303c677 100644 --- a/fuzzing/corpus/corpus.go +++ b/fuzzing/corpus/corpus.go @@ -3,6 +3,14 @@ package corpus import ( "bytes" "fmt" + "math/big" + "os" + "path/filepath" + "regexp" + "strconv" + "sync" + "time" + "github.com/crytic/medusa/chain" "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/coverage" @@ -12,13 +20,6 @@ import ( "github.com/crytic/medusa/utils/randomutils" "github.com/ethereum/go-ethereum/common" "github.com/google/uuid" - "math/big" - "os" - "path/filepath" - "regexp" - "strconv" - "sync" - "time" "github.com/crytic/medusa/fuzzing/contracts" ) @@ -363,7 +364,7 @@ func (c *Corpus) Initialize(baseTestChain *chain.TestChain, contractDefinitions covMaps := coverage.GetCoverageTracerResults(messageResults) _, _, covErr := c.coverageMaps.Update(covMaps) if covErr != nil { - return 0, 0, err + return 0, 0, covErr } } }