Skip to content

Commit 0112ddc

Browse files
anishnaiksmonicas
andauthored
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 <[email protected]>
1 parent 350ee4b commit 0112ddc

File tree

11 files changed

+422
-4
lines changed

11 files changed

+422
-4
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ jobs:
214214

215215
- name: Install Python dependencies
216216
run: |
217-
pip3 install --no-cache-dir solc-select crytic-compile
217+
pip3 install --no-cache-dir solc-select slither-analyzer
218218
219219
- name: Install solc
220220
run: |

cmd/fuzz_flags.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ func addFuzzFlags() error {
6969
// Exploration mode
7070
fuzzCmd.Flags().Bool("explore", false, "enables exploration mode")
7171

72+
// Run slither on-the-fly
73+
fuzzCmd.Flags().Bool("use-slither", false, "runs slither")
7274
return nil
7375
}
7476

@@ -193,5 +195,17 @@ func updateProjectConfigWithFuzzFlags(cmd *cobra.Command, projectConfig *config.
193195
projectConfig.Fuzzing.Testing.OptimizationTesting.Enabled = false
194196
}
195197
}
198+
199+
// Update configuration to run slither
200+
if cmd.Flags().Changed("use-slither") {
201+
useSlither, err := cmd.Flags().GetBool("use-slither")
202+
if err != nil {
203+
return err
204+
}
205+
if useSlither {
206+
projectConfig.Slither.UseSlither = true
207+
}
208+
}
209+
196210
return nil
197211
}

compilation/types/slither.go

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
package types
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"github.com/crytic/medusa/logging"
7+
"os"
8+
"os/exec"
9+
"time"
10+
)
11+
12+
// SlitherConfig determines whether to run slither and whether and where to cache the results from slither
13+
type SlitherConfig struct {
14+
// UseSlither determines whether to use slither. If CachePath is non-empty, then the cached results will be
15+
// attempted to be used. Otherwise, slither will be run.
16+
UseSlither bool `json:"useSlither"`
17+
// CachePath determines the path where the slither cache file will be located
18+
CachePath string `json:"cachePath"`
19+
}
20+
21+
// NewDefaultSlitherConfig provides a default configuration to run slither. The default configuration enables the
22+
// running of slither with the use of a cache.
23+
func NewDefaultSlitherConfig() (*SlitherConfig, error) {
24+
return &SlitherConfig{
25+
UseSlither: true,
26+
CachePath: "slither_results.json",
27+
}, nil
28+
}
29+
30+
// SlitherResults describes a data structures that holds the interesting constants returned from slither
31+
type SlitherResults struct {
32+
// Constants holds the constants extracted by slither
33+
Constants []Constant `json:"constantsUsed"`
34+
}
35+
36+
// Constant defines a constant that was extracted by slither while parsing the compilation target
37+
type Constant struct {
38+
// Type represents the ABI type of the constant
39+
Type string `json:"type"`
40+
// Value represents the value of the constant
41+
Value string `json:"value"`
42+
}
43+
44+
// RunSlither on the provided compilation target. RunSlither will use cached results if they exist and write to the
45+
// cache if we have not written to the cache already. A SlitherResults data structure is returned.
46+
func (s *SlitherConfig) RunSlither(target string) (*SlitherResults, error) {
47+
// Return early if we do not want to run slither
48+
if !s.UseSlither {
49+
return nil, nil
50+
}
51+
52+
// Use the cached slither output if it exists
53+
var haveCachedResults bool
54+
var out []byte
55+
var err error
56+
if s.CachePath != "" {
57+
// Check to see if the file exists in the first place.
58+
// If not, we will re-run slither
59+
if _, err = os.Stat(s.CachePath); os.IsNotExist(err) {
60+
logging.GlobalLogger.Info("No Slither cached results found at ", s.CachePath)
61+
haveCachedResults = false
62+
} else {
63+
// We found the cached file
64+
if out, err = os.ReadFile(s.CachePath); err != nil {
65+
return nil, err
66+
}
67+
haveCachedResults = true
68+
logging.GlobalLogger.Info("Using cached Slither results found at ", s.CachePath)
69+
}
70+
}
71+
72+
// Run slither if we do not have cached results, or we cannot find the cached results
73+
if !haveCachedResults {
74+
// Log the command
75+
cmd := exec.Command("slither", target, "--ignore-compile", "--print", "echidna", "--json", "-")
76+
logging.GlobalLogger.Info("Running Slither:\n", cmd.String())
77+
78+
// Run slither
79+
start := time.Now()
80+
out, err = cmd.CombinedOutput()
81+
if err != nil {
82+
return nil, err
83+
}
84+
logging.GlobalLogger.Info("Finished running Slither in ", time.Since(start).Round(time.Second))
85+
}
86+
87+
// Capture the slither results
88+
var slitherResults SlitherResults
89+
err = json.Unmarshal(out, &slitherResults)
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
// Cache the results if we have not cached before. We have also already checked that the output is well-formed
95+
// (through unmarshal) so we should be safe.
96+
if !haveCachedResults && s.CachePath != "" {
97+
// Cache the data
98+
err = os.WriteFile(s.CachePath, out, 0644)
99+
if err != nil {
100+
// If we are unable to write to the cache, we should log the error but continue
101+
logging.GlobalLogger.Warn("Failed to cache Slither results at ", s.CachePath, " due to an error:", err)
102+
// It is possible for os.WriteFile to create a partially written file so it is best to try to delete it
103+
if _, err = os.Stat(s.CachePath); err == nil {
104+
// We will not handle the error of os.Remove since we have already checked for the file's existence
105+
// and we have the right permissions.
106+
os.Remove(s.CachePath)
107+
}
108+
}
109+
}
110+
111+
return &slitherResults, nil
112+
}
113+
114+
// UnmarshalJSON unmarshals the slither output into a Slither type
115+
func (s *SlitherResults) UnmarshalJSON(d []byte) error {
116+
// Extract the top-level JSON object
117+
var obj map[string]json.RawMessage
118+
if err := json.Unmarshal(d, &obj); err != nil {
119+
return err
120+
}
121+
122+
// Decode success and error. They are always present in the slither output
123+
var success bool
124+
var slitherError string
125+
if err := json.Unmarshal(obj["success"], &success); err != nil {
126+
return err
127+
}
128+
129+
if err := json.Unmarshal(obj["error"], &slitherError); err != nil {
130+
return err
131+
}
132+
133+
// If success is not true or there is a non-empty error string, return early
134+
if !success || slitherError != "" {
135+
if slitherError != "" {
136+
return errors.New(slitherError)
137+
}
138+
return errors.New("slither returned a failure during parsing")
139+
}
140+
141+
// Now we will extract the constants
142+
s.Constants = make([]Constant, 0)
143+
144+
// Iterate through the JSON object until we get to the constants_used key
145+
// First, retrieve the results
146+
var results map[string]json.RawMessage
147+
if err := json.Unmarshal(obj["results"], &results); err != nil {
148+
return err
149+
}
150+
151+
// Retrieve the printers data
152+
var printers []json.RawMessage
153+
if err := json.Unmarshal(results["printers"], &printers); err != nil {
154+
return err
155+
}
156+
157+
// Since we are running the echidna printer, we know that the first element is the one we care about
158+
var echidnaPrinter map[string]json.RawMessage
159+
if err := json.Unmarshal(printers[0], &echidnaPrinter); err != nil {
160+
return err
161+
}
162+
163+
// We need to de-serialize the description in two separate steps because go is dumb sometimes
164+
var descriptionString string
165+
if err := json.Unmarshal(echidnaPrinter["description"], &descriptionString); err != nil {
166+
return err
167+
}
168+
var description map[string]json.RawMessage
169+
if err := json.Unmarshal([]byte(descriptionString), &description); err != nil {
170+
return err
171+
}
172+
173+
// Capture all the constants extracted across all the contracts in scope
174+
var constantsInContracts map[string]json.RawMessage
175+
if err := json.Unmarshal(description["constants_used"], &constantsInContracts); err != nil {
176+
return err
177+
}
178+
179+
// Iterate across the constants in each contract
180+
for _, constantsInContract := range constantsInContracts {
181+
// Capture all the constants in a given function
182+
var constantsInFunctions map[string]json.RawMessage
183+
if err := json.Unmarshal(constantsInContract, &constantsInFunctions); err != nil {
184+
return err
185+
}
186+
187+
// Iterate across each function
188+
for _, constantsInFunction := range constantsInFunctions {
189+
// Each constant is provided as its own list, so we need to create a matrix
190+
var constants [][]Constant
191+
if err := json.Unmarshal(constantsInFunction, &constants); err != nil {
192+
return err
193+
}
194+
for _, constant := range constants {
195+
// Slither outputs the value of a constant as a list
196+
// However we know there can be only 1 so we take index 0
197+
s.Constants = append(s.Constants, constant[0])
198+
}
199+
}
200+
}
201+
202+
return nil
203+
}

docs/src/project_configuration/overview.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ configuration is a `.json` file that is broken down into five core components.
77
- [Testing Configuration](./testing_config.md): The testing configuration dictates how and what `medusa` should fuzz test.
88
- [Chain Configuration](./chain_config.md): The chain configuration dictates how `medusa`'s underlying blockchain should be configured.
99
- [Compilation Configuration](./compilation_config.md): The compilation configuration dictates how to compile the fuzzing target.
10+
- [Slither Configuration](./slither_config.md): The Slither configuration dictates whether Slither should be used in
11+
`medusa` and whether the results from Slither should be cached.
1012
- [Logging Configuration](./logging_config.md): The logging configuration dictates when and where to log events.
1113

1214
To generate a project configuration file, run [`medusa init`](../cli/init.md).
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Slither Configuration
2+
3+
The [Slither](https://github.com/crytic/slither) configuration defines the parameters for using Slither in `medusa`.
4+
Currently, we use Slither to extract interesting constants from the target system. These constants are then used in the
5+
fuzzing process to try to increase coverage. Note that if Slither fails to run for some reason, we will still try our
6+
best to mine constants from each contract's AST so don't worry!
7+
8+
- > 🚩 We _highly_ recommend using Slither and caching the results. Basically, don't change this configuration unless
9+
> absolutely necessary. The constants identified by Slither are shown to greatly improve system coverage and caching
10+
> the results will improve the speed of medusa.
11+
12+
### `useSlither`
13+
14+
- **Type**: Boolean
15+
- **Description**: If `true`, Slither will be run on the target system and useful constants will be extracted for fuzzing.
16+
If `cachePath` is a non-empty string (which it is by default), then `medusa` will first check the cache before running
17+
Slither.
18+
- **Default**: `true`
19+
20+
### `cachePath`
21+
22+
- **Type**: String
23+
- **Description**: If `cachePath` is non-empty, Slither's results will be cached on disk. When `medusa` is re-run, these
24+
cached results will be used. We do this for performance reasons since re-running Slither each time `medusa` is restarted
25+
is computationally intensive for complex projects. We recommend disabling caching (by making `cachePath` an empty string)
26+
if the target codebase changes. If the code remains constant during the fuzzing campaign, we recommend to use the cache.
27+
- **Default**: `slither_results.json`

fuzzing/config/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"errors"
66
"fmt"
7+
"github.com/crytic/medusa/compilation/types"
78
"math/big"
89
"os"
910

@@ -27,6 +28,9 @@ type ProjectConfig struct {
2728
// Compilation describes the configuration used to compile the underlying project.
2829
Compilation *compilation.CompilationConfig `json:"compilation"`
2930

31+
// Slither describes the configuration for running slither
32+
Slither *types.SlitherConfig `json:"slither"`
33+
3034
// Logging describes the configuration used for logging to file and console
3135
Logging LoggingConfig `json:"logging"`
3236
}

fuzzing/config/config_defaults.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package config
22

33
import (
4+
"github.com/crytic/medusa/compilation/types"
45
"math/big"
56

67
testChainConfig "github.com/crytic/medusa/chain/config"
@@ -31,6 +32,12 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) {
3132
return nil, err
3233
}
3334

35+
// Obtain a default slither configuration
36+
slitherConfig, err := types.NewDefaultSlitherConfig()
37+
if err != nil {
38+
return nil, err
39+
}
40+
3441
// Create a project configuration
3542
projectConfig := &ProjectConfig{
3643
Fuzzing: FuzzingConfig{
@@ -88,6 +95,7 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) {
8895
TestChainConfig: *chainConfig,
8996
},
9097
Compilation: compilationConfig,
98+
Slither: slitherConfig,
9199
Logging: LoggingConfig{
92100
Level: zerolog.InfoLevel,
93101
LogDirectory: "",

fuzzing/fuzzer.go

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ type Fuzzer struct {
6060
// contractDefinitions defines targets to be fuzzed once their deployment is detected. They are derived from
6161
// compilations.
6262
contractDefinitions fuzzerTypes.Contracts
63+
// slitherResults holds the results obtained from slither. At the moment we do not have use for storing this in the
64+
// Fuzzer but down the line we can use slither for other capabilities that may require storage of the results.
65+
slitherResults *compilationTypes.SlitherResults
66+
6367
// baseValueSet represents a valuegeneration.ValueSet containing input values for our fuzz tests.
6468
baseValueSet *valuegeneration.ValueSet
6569

@@ -284,16 +288,44 @@ func (f *Fuzzer) ReportTestCaseFinished(testCase TestCase) {
284288
// AddCompilationTargets takes a compilation and updates the Fuzzer state with additional Fuzzer.ContractDefinitions
285289
// definitions and Fuzzer.BaseValueSet values.
286290
func (f *Fuzzer) AddCompilationTargets(compilations []compilationTypes.Compilation) {
287-
// Loop for each contract in each compilation and deploy it to the test chain
291+
var seedFromAST bool
292+
293+
// No need to handle the error here since having compilation artifacts implies that we used a supported
294+
// platform configuration
295+
platformConfig, _ := f.config.Compilation.GetPlatformConfig()
296+
297+
// Retrieve the compilation target for slither
298+
target := platformConfig.GetTarget()
299+
300+
// Run slither and handle errors
301+
slitherResults, err := f.config.Slither.RunSlither(target)
302+
if err != nil || slitherResults == nil {
303+
if err != nil {
304+
f.logger.Warn("Failed to run slither", err)
305+
}
306+
seedFromAST = true
307+
}
308+
309+
// If we have results and there were no errors, we will seed the value set using the slither results
310+
if !seedFromAST {
311+
f.slitherResults = slitherResults
312+
// Seed our base value set with the constants extracted by Slither
313+
f.baseValueSet.SeedFromSlither(slitherResults)
314+
}
315+
316+
// Capture all the contract definitions, functions, and cache the source code
288317
for i := 0; i < len(compilations); i++ {
289318
// Add our compilation to the list and get a reference to it.
290319
f.compilations = append(f.compilations, compilations[i])
291320
compilation := &f.compilations[len(f.compilations)-1]
292321

293322
// Loop for each source
294323
for sourcePath, source := range compilation.SourcePathToArtifact {
295-
// Seed our base value set from every source's AST
296-
f.baseValueSet.SeedFromAst(source.Ast)
324+
// Seed from the contract's AST if we did not use slither or failed to do so
325+
if seedFromAST {
326+
// Seed our base value set from every source's AST
327+
f.baseValueSet.SeedFromAst(source.Ast)
328+
}
297329

298330
// Loop for every contract and register it in our contract definitions
299331
for contractName := range source.Contracts {

0 commit comments

Comments
 (0)