Skip to content

Commit

Permalink
Merge branch 'master' into dev/v0.1.7
Browse files Browse the repository at this point in the history
  • Loading branch information
anishnaik authored Oct 15, 2024
2 parents f70a195 + e3120ea commit ed99f25
Show file tree
Hide file tree
Showing 32 changed files with 592 additions and 160 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
needs: [lint, test]
strategy:
matrix:
environment: [ubuntu-latest, macos-12, macos-14, windows-latest]
environment: [ubuntu-latest, macos-13, macos-14, windows-latest]
permissions:
contents: read
id-token: write
Expand Down Expand Up @@ -165,7 +165,7 @@ jobs:
test:
strategy:
matrix:
environment: [ubuntu-latest, macos-12, macos-14, windows-latest]
environment: [ubuntu-latest, macos-13, macos-14, windows-latest]

runs-on: ${{ matrix.environment }}
timeout-minutes: 20
Expand Down
2 changes: 1 addition & 1 deletion CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -1 +1 @@
* @Xenomega @anishnaik @0xalpharush
* @Xenomega @anishnaik
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
`medusa` is a cross-platform [go-ethereum](https://github.com/ethereum/go-ethereum/)-based smart contract fuzzer inspired by [Echidna](https://github.com/crytic/echidna).
It provides parallelized fuzz testing of smart contracts through CLI, or its Go API that allows custom user-extended testing methodology.

**Disclaimer**: Please note that `medusa` is an **experimental** smart contract fuzzer. Currently, it should _not_ be adopted into production systems. We intend for `medusa` to reach the same capabilities and maturity that Echidna has. Until then, be careful using `medusa` as your primary smart contract fuzz testing solution. Additionally, please be aware that the Go-level testing API is still **under development** and is subject to breaking changes.
**Disclaimer**: The Go-level testing API is still **under development** and is subject to breaking changes.

## Features

Expand All @@ -29,6 +29,23 @@ cd docs
mdbook serve
```

## Install

MacOS users can install the latest release of `medusa` using Homebrew:

```shell

brew install medusa
```

The master branch can be installed using the following command:

```shell
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).

## Contributing

For information about how to contribute to this project, check out the [CONTRIBUTING](./CONTRIBUTING.md) guidelines.
Expand Down
3 changes: 3 additions & 0 deletions chain/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ type TestChainConfig struct {
// CheatCodeConfig indicates the configuration for EVM cheat codes to use.
CheatCodeConfig CheatCodeConfig `json:"cheatCodes"`

// SkipAccountChecks skips account pre-checks like nonce validation and disallowing non-EOA tx senders (this is done in eth_call, for instance).
SkipAccountChecks bool `json:"skipAccountChecks"`

// ContractAddressOverrides describes contracts that are going to be deployed at deterministic addresses
ContractAddressOverrides map[common.Hash]common.Address `json:"contractAddressOverrides,omitempty"`
}
Expand Down
1 change: 1 addition & 0 deletions chain/config/config_defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ func DefaultTestChainConfig() (*TestChainConfig, error) {
CheatCodesEnabled: true,
EnableFFI: false,
},
SkipAccountChecks: true,
}

// Return the generated configuration.
Expand Down
2 changes: 1 addition & 1 deletion chain/test_chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,7 @@ func TestChainCloning(t *testing.T) {
})
}

// TestCallSequenceReplayMatchSimple creates a TestChain, sends some messages to it, then creates another chain which
// TestChainCallSequenceReplayMatchSimple creates a TestChain, sends some messages to it, then creates another chain which
// it replays the same sequence on. It ensures that the ending state is the same.
// Note: this does not set block timestamps or other data that might be non-deterministic.
// This does not test replaying with a previous call sequence with different timestamps, etc. It expects the TestChain
Expand Down
2 changes: 1 addition & 1 deletion compilation/platforms/crytic_compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ func (c *CryticCompilationConfig) Compile() ([]types.Compilation, string, error)
}

// Retrieve the source unit ID
sourceUnitId := ast.GetSourceUnitID()
sourceUnitId := types.GetSrcMapSourceUnitID(ast.Src)
compilation.SourcePathToArtifact[sourcePath] = types.SourceArtifact{
// TODO: Our types.AST is not the same as the original AST but we could parse it and avoid using "any"
Ast: source.AST,
Expand Down
2 changes: 1 addition & 1 deletion compilation/platforms/solc.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func (s *SolcCompilationConfig) Compile() ([]types.Compilation, string, error) {
}

// Get the source unit ID
sourceUnitId := ast.GetSourceUnitID()
sourceUnitId := types.GetSrcMapSourceUnitID(ast.Src)
// Construct our compiled source object
compilation.SourcePathToArtifact[sourcePath] = types.SourceArtifact{
// TODO our types.AST is not the same as the original AST but we could parse it and avoid using "any"
Expand Down
120 changes: 105 additions & 15 deletions compilation/types/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,84 @@ const (

// Node interface represents a generic AST node
type Node interface {
// GetNodeType returns solc's node type e.g. FunctionDefinition, ContractDefinition.
GetNodeType() string
}

// FunctionDefinition is the function definition node
type FunctionDefinition struct {
// NodeType represents the node type (currently we only evaluate source unit node types)
NodeType string `json:"nodeType"`
// Src is the source file for this AST
Src string `json:"src"`
Name string `json:"name,omitempty"`
}

func (s FunctionDefinition) GetNodeType() string {
return s.NodeType
}

// ContractDefinition is the contract definition node
type ContractDefinition struct {
// NodeType represents the AST node type (note that it will always be a contract definition)
// NodeType represents the node type (currently we only evaluate source unit node types)
NodeType string `json:"nodeType"`
// Nodes is a list of Nodes within the AST
Nodes []Node `json:"nodes"`
// Src is the source file for this AST
Src string `json:"src"`
// CanonicalName is the name of the contract definition
CanonicalName string `json:"canonicalName,omitempty"`
// Kind is a ContractKind that represents what type of contract definition this is (contract, interface, or library)
Kind ContractKind `json:"contractKind,omitempty"`
}

// GetNodeType implements the Node interface and returns the node type for the contract definition
func (s ContractDefinition) GetNodeType() string {
return s.NodeType
}

func (c *ContractDefinition) UnmarshalJSON(data []byte) error {
// Unmarshal the top-level AST into our own representation. Defer the unmarshaling of all the individual nodes until later
type Alias ContractDefinition
aux := &struct {
Nodes []json.RawMessage `json:"nodes"`

*Alias
}{
Alias: (*Alias)(c),
}

if err := json.Unmarshal(data, &aux); err != nil {
return err
}

// Iterate through all the nodes of the contract definition
for _, nodeData := range aux.Nodes {
// Unmarshal the node data to retrieve the node type
var nodeType struct {
NodeType string `json:"nodeType"`
}
if err := json.Unmarshal(nodeData, &nodeType); err != nil {
return err
}

// Unmarshal the contents of the node based on the node type
switch nodeType.NodeType {
case "FunctionDefinition":
// If this is a function definition, unmarshal it
var functionDefinition FunctionDefinition
if err := json.Unmarshal(nodeData, &functionDefinition); err != nil {
return err
}
c.Nodes = append(c.Nodes, functionDefinition)
default:
continue
}
}

return nil

}

// AST is the abstract syntax tree
type AST struct {
// NodeType represents the node type (currently we only evaluate source unit node types)
Expand All @@ -48,7 +108,6 @@ type AST struct {
Src string `json:"src"`
}

// UnmarshalJSON unmarshals from JSON
func (a *AST) UnmarshalJSON(data []byte) error {
// Unmarshal the top-level AST into our own representation. Defer the unmarshaling of all the individual nodes until later
type Alias AST
Expand All @@ -62,11 +121,6 @@ func (a *AST) UnmarshalJSON(data []byte) error {
return err
}

// Check if nodeType is "SourceUnit". Return early otherwise
if aux.NodeType != "SourceUnit" {
return nil
}

// Iterate through all the nodes of the source unit
for _, nodeData := range aux.Nodes {
// Unmarshal the node data to retrieve the node type
Expand All @@ -78,31 +132,37 @@ func (a *AST) UnmarshalJSON(data []byte) error {
}

// Unmarshal the contents of the node based on the node type
var node Node
switch nodeType.NodeType {
case "ContractDefinition":
// If this is a contract definition, unmarshal it
var contractDefinition ContractDefinition
if err := json.Unmarshal(nodeData, &contractDefinition); err != nil {
return err
}
node = contractDefinition
a.Nodes = append(a.Nodes, contractDefinition)

case "FunctionDefinition":
// If this is a function definition, unmarshal it
var functionDefinition FunctionDefinition
if err := json.Unmarshal(nodeData, &functionDefinition); err != nil {
return err
}
a.Nodes = append(a.Nodes, functionDefinition)

// TODO: Add cases for other node types as needed
default:
continue
}

// Append the node
a.Nodes = append(a.Nodes, node)
}

return nil
}

// GetSourceUnitID returns the source unit ID based on the source of the AST
func (a *AST) GetSourceUnitID() int {
// GetSrcMapSourceUnitID returns the source unit ID based on the source of the AST
func GetSrcMapSourceUnitID(src string) int {
re := regexp.MustCompile(`[0-9]*:[0-9]*:([0-9]*)`)
sourceUnitCandidates := re.FindStringSubmatch(a.Src)
sourceUnitCandidates := re.FindStringSubmatch(src)

if len(sourceUnitCandidates) == 2 { // FindStringSubmatch includes the whole match as the first element
sourceUnit, err := strconv.Atoi(sourceUnitCandidates[1])
Expand All @@ -112,3 +172,33 @@ func (a *AST) GetSourceUnitID() int {
}
return -1
}

// GetSrcMapStart returns the byte offset where the function definition starts in the source file
func GetSrcMapStart(src string) int {
// 95:42:0 returns 95
re := regexp.MustCompile(`([0-9]*):[0-9]*:[0-9]*`)
startCandidates := re.FindStringSubmatch(src)

if len(startCandidates) == 2 { // FindStringSubmatch includes the whole match as the first element
start, err := strconv.Atoi(startCandidates[1])
if err == nil {
return start
}
}
return -1
}

// GetSrcMapLength returns the length of the function definition in bytes
func GetSrcMapLength(src string) int {
// 95:42:0 returns 42
re := regexp.MustCompile(`[0-9]*:([0-9]*):[0-9]*`)
endCandidates := re.FindStringSubmatch(src)

if len(endCandidates) == 2 { // FindStringSubmatch includes the whole match as the first element
end, err := strconv.Atoi(endCandidates[1])
if err == nil {
return end
}
}
return -1
}
43 changes: 42 additions & 1 deletion docs/src/coverage_reports.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,44 @@
# Coverage Reports

WIP
## Generating HTML Report from LCOV

Enable coverage reporting by setting the `corpusDirectory` key in the configuration file and setting the `coverageReports` key to `["lcov", "html"]`.

```json
{
"corpusDirectory": "corpus",
"coverageReports": ["lcov", "html"]
}
```

### Install lcov and genhtml

Linux:

```bash
apt-get install lcov
```

MacOS:

```bash
brew install lcov
```

### Generate LCOV Report

```bash

genhtml corpus/coverage/lcov.info --output-dir corpus --rc derive_function_end_line=0
```

> [!WARNING]
> ** The `derive_function_end_line` flag is required to prevent the `genhtml` tool from crashing when processing the Solidity source code. **
Open the `corpus/index.html` file in your browser or follow the steps to use VSCode below.

### View Coverage Report in VSCode with Coverage Gutters

Install the [Coverage Gutters](https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters) extension.

Then, right click in a project file and select `Coverage Gutters: Display Coverage`.
6 changes: 6 additions & 0 deletions docs/src/project_configuration/chain_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ The chain configuration defines the parameters for setting up `medusa`'s underly
- > 🚩 Setting `codeSizeCheckDisabled` to `false` is not recommended since it complicates the fuzz testing process.
- **Default**: `true`

### `skipAccountChecks`

- **Type**: Boolean
- **Description**: If `true`, account-related checks (nonce validation, transaction origin must be an EOA) are disabled in `go-ethereum`.
- **Default**: `true`

## Cheatcode Configuration

### `cheatCodesEnabled`
Expand Down
7 changes: 7 additions & 0 deletions docs/src/project_configuration/fuzzing_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ The fuzzing configuration defines the parameters for the fuzzing campaign.
can then be re-used/mutated by the fuzzer during the next fuzzing campaign.
- **Default**: ""

### `coverageFormats`

- **Type**: [String] (e.g. `["lcov"]`)
- **Description**: The coverage reports to generate after the fuzzing campaign has completed. The coverage reports are saved
in the `coverage` directory within `crytic-export/` or `corpusDirectory` if configured.
- **Default**: `["lcov", "html"]`

### `targetContracts`

- **Type**: [String] (e.g. `[FirstContract, SecondContract, ThirdContract]`)
Expand Down
9 changes: 7 additions & 2 deletions docs/src/static/medusa.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
"workerResetLimit": 50,
"timeout": 0,
"testLimit": 0,
"shrinkLimit": 5000,
"callSequenceLength": 100,
"corpusDirectory": "",
"coverageEnabled": true,
"targetContracts": [],
"predeployedContracts": {},
"targetContractsBalances": [],
"constructorArgs": {},
"deployerAddress": "0x30000",
Expand Down Expand Up @@ -45,14 +47,17 @@
"optimizationTesting": {
"enabled": true,
"testPrefixes": ["optimize_"]
}
},
"targetFunctionSignatures": [],
"excludeFunctionSignatures": []
},
"chainConfig": {
"codeSizeCheckDisabled": true,
"cheatCodes": {
"cheatCodesEnabled": true,
"enableFFI": false
}
},
"skipAccountChecks": true
}
},
"compilation": {
Expand Down
2 changes: 1 addition & 1 deletion fuzzing/calls/call_sequence_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ func ExecuteCallSequence(chain *chain.TestChain, callSequence CallSequence) (Cal
return ExecuteCallSequenceIteratively(chain, fetchElementFunc, nil)
}

// ExecuteCallSequenceWithTracer attaches an executiontracer.ExecutionTracer to ExecuteCallSequenceIteratively and attaches execution traces to the call sequence elements.
// ExecuteCallSequenceWithExecutionTracer attaches an executiontracer.ExecutionTracer to ExecuteCallSequenceIteratively and attaches execution traces to the call sequence elements.
func ExecuteCallSequenceWithExecutionTracer(testChain *chain.TestChain, contractDefinitions contracts.Contracts, callSequence CallSequence, verboseTracing bool) (CallSequence, error) {
// Create a new execution tracer
executionTracer := executiontracer.NewExecutionTracer(contractDefinitions, testChain.CheatCodeContracts())
Expand Down
Loading

0 comments on commit ed99f25

Please sign in to comment.