Skip to content

Commit

Permalink
Merge branch 'master' into feat/capture-return-values
Browse files Browse the repository at this point in the history
  • Loading branch information
anishnaik authored Jan 14, 2025
2 parents 163eda8 + 5642046 commit d9b5197
Show file tree
Hide file tree
Showing 20 changed files with 726 additions and 20 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 10 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
46 changes: 45 additions & 1 deletion cmd/fuzz_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,16 @@ 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")

// Run slither on-the-fly
fuzzCmd.Flags().Bool("use-slither", false, "runs slither")
return nil
}

Expand Down Expand Up @@ -163,5 +171,41 @@ func updateProjectConfigWithFuzzFlags(cmd *cobra.Command, projectConfig *config.
return err
}
}

// 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") {
explore, err := cmd.Flags().GetBool("explore")
if err != nil {
return err
}
if explore {
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
}
}

// 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
}
203 changes: 203 additions & 0 deletions compilation/types/slither.go
Original file line number Diff line number Diff line change
@@ -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
}
19 changes: 19 additions & 0 deletions docs/src/cli/fuzz.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -129,3 +139,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
```
21 changes: 21 additions & 0 deletions docs/src/getting_started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/src/project_configuration/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Loading

0 comments on commit d9b5197

Please sign in to comment.