From 0112ddc7a540d973b611def13c2db364d5a8d491 Mon Sep 17 00:00:00 2001 From: anishnaik Date: Wed, 8 Jan 2025 18:39:49 -0500 Subject: [PATCH] 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)) + } + } +}