Skip to content

Commit

Permalink
call property and optimization tests even if they aren't targeted
Browse files Browse the repository at this point in the history
  • Loading branch information
0xalpharush committed Jul 18, 2024
1 parent 27d1623 commit 5a66fa2
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 31 deletions.
2 changes: 2 additions & 0 deletions docs/src/project_configuration/testing_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,14 @@ contract MyContract {

- **Type**: [String]
- **Description**: A list of function signatures that the fuzzer should exclusively target by omitting calls to other signatures. The signatures should specify the contract name and signature in the ABI format like `Contract:func(uint256,bytes32)`.
> **Note**: Property and optimization tests will always be called even if they are not explicitly specified in this list.
- **Default**: `[]`

### `excludeFunctionSignatures`:

- **Type**: [String]
- **Description**: A list of function signatures that the fuzzer should exclude from the fuzzing campaign. The signatures should specify the contract name and signature in the ABI format like `Contract:func(uint256,bytes32)`.
> **Note**: Property and optimization tests will always be called and cannot be excluded.
- **Default**: `[]`


Expand Down
37 changes: 12 additions & 25 deletions fuzzing/contracts/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,32 +39,25 @@ type Contract struct {
// compilation describes the compilation which contains the compiledContract.
compilation *types.Compilation

// candidateMethods are the methods that can be called on the contract after targeting/excluding is performed.
candidateMethods []abi.Method

// propertyTestMethods are the methods that are property tests (subset of candidateMethods).
// propertyTestMethods are the methods that are property tests.
propertyTestMethods []abi.Method

// optimizationTestMethods are the methods that are optimization tests (subset of candidateMethods).
// optimizationTestMethods are the methods that are optimization tests.
optimizationTestMethods []abi.Method

// assertionTestMethods are the methods that are assertion tests (subset of candidateMethods).
// All candidateMethods that are not property or optimization tests are assertion tests.
// assertionTestMethods are ALL other methods that are not property or optimization tests by default.
// If configured, the methods will be targeted or excluded based on the targetFunctionSignatures
// and excludedFunctionSignatures, respectively.
assertionTestMethods []abi.Method
}

// NewContract returns a new Contract instance with the provided information.
func NewContract(name string, sourcePath string, compiledContract *types.CompiledContract, compilation *types.Compilation) *Contract {
var methods []abi.Method
for _, method := range compiledContract.Abi.Methods {
methods = append(methods, method)
}
return &Contract{
name: name,
sourcePath: sourcePath,
compiledContract: compiledContract,
compilation: compilation,
candidateMethods: methods,
}
}

Expand All @@ -79,25 +72,25 @@ func containsMethod(methods []string, target string) bool {

func (c *Contract) WithTargetMethods(target []string) *Contract {
var candidateMethods []abi.Method
for _, method := range c.candidateMethods {
for _, method := range c.assertionTestMethods {
canonicalSig := strings.Join([]string{c.name, method.Sig}, ".")
if containsMethod(target, canonicalSig) {
candidateMethods = append(candidateMethods, method)
}
}
c.candidateMethods = candidateMethods
c.assertionTestMethods = candidateMethods
return c
}

func (c *Contract) WithExcludedMethods(excludedMethods []string) *Contract {
var candidateMethods []abi.Method
for _, method := range c.candidateMethods {
for _, method := range c.assertionTestMethods {
canonicalSig := strings.Join([]string{c.name, method.Sig}, ".")
if !containsMethod(excludedMethods, canonicalSig) {
candidateMethods = append(candidateMethods, method)
}
}
c.candidateMethods = candidateMethods
c.assertionTestMethods = candidateMethods
return c
}

Expand All @@ -121,23 +114,17 @@ func (c *Contract) Compilation() *types.Compilation {
return c.compilation
}

// CandidateMethods returns the methods that can be called on the contract after targeting/excluding is performed.
func (c *Contract) CandidateMethods() []abi.Method {
return c.candidateMethods
}

// PropertyTestMethods returns the methods that are property tests (subset of CandidateMethods).
// PropertyTestMethods returns the methods that are property tests.
func (c *Contract) PropertyTestMethods() []abi.Method {
return c.propertyTestMethods
}

// OptimizationTestMethods returns the methods that are optimization tests (subset of CandidateMethods).
// OptimizationTestMethods returns the methods that are optimization tests.
func (c *Contract) OptimizationTestMethods() []abi.Method {
return c.optimizationTestMethods
}

// AssertionTestMethods returns the methods that are assertion tests (subset of CandidateMethods).
// All CandidateMethods that are not property or optimization tests are assertion tests.
// AssertionTestMethods returns the methods that are assertion tests.
func (c *Contract) AssertionTestMethods() []abi.Method {
return c.assertionTestMethods
}
Expand Down
9 changes: 6 additions & 3 deletions fuzzing/fuzzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,12 @@ func (f *Fuzzer) AddCompilationTargets(compilations []compilationTypes.Compilati
for contractName := range source.Contracts {
contract := source.Contracts[contractName]
contractDefinition := fuzzerTypes.NewContract(contractName, sourcePath, &contract, compilation)

// Sort available methods by type
assertionTestMethods, propertyTestMethods, optimizationTestMethods := fuzzingutils.BinTestByType(&contract, f.config.Fuzzing.Testing)
contractDefinition.AddTestMethods(assertionTestMethods, propertyTestMethods, optimizationTestMethods)

// Filter and record methods available for assertion testing. Property and optimization tests are always run.
if len(targetMethods) > 0 {
// Only consider methods that are in the target methods list
contractDefinition = contractDefinition.WithTargetMethods(targetMethods)
Expand All @@ -307,9 +313,6 @@ func (f *Fuzzer) AddCompilationTargets(compilations []compilationTypes.Compilati
// Consider all methods except those in the exclude methods list
contractDefinition = contractDefinition.WithExcludedMethods(excludeMethods)
}
// Filter and record methods available for testing
assertionTestMethods, propertyTestMethods, optimizationTestMethods := fuzzingutils.BinTestByType(contractDefinition.CandidateMethods(), f.config.Fuzzing.Testing)
contractDefinition.AddTestMethods(assertionTestMethods, propertyTestMethods, optimizationTestMethods)

f.contractDefinitions = append(f.contractDefinitions, contractDefinition)
}
Expand Down
39 changes: 39 additions & 0 deletions fuzzing/fuzzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/hex"
"math/big"
"math/rand"
"reflect"
"testing"

"github.com/crytic/medusa/fuzzing/executiontracer"
Expand Down Expand Up @@ -870,3 +871,41 @@ func TestDeploymentOrderWithCoverage(t *testing.T) {
},
})
}

func TestTargetingFuncSignatures(t *testing.T) {
targets := []string{"TestContract.f(), TestContract.g()"}
runFuzzerTest(t, &fuzzerSolcFileTest{
filePath: "testdata/contracts/filtering/target_and_exclude.sol",
configUpdates: func(config *config.ProjectConfig) {
config.Fuzzing.TargetContracts = []string{"TestContract"}
config.Fuzzing.Testing.TargetFunctionSignatures = targets
},
method: func(f *fuzzerTestContext) {
for _, contract := range f.fuzzer.ContractDefinitions() {
// The targets should be the only functions tested, excluding h and i
reflect.DeepEqual(contract.AssertionTestMethods(), targets)
reflect.DeepEqual(contract.PropertyTestMethods(), []string{"TestContract.property_a()"})
// ALL properties and optimizations should be tested
reflect.DeepEqual(contract.OptimizationTestMethods(), []string{"TestContract.optimize_b()"})
}
}})
}

func TestExcludeFunctionSignatures(t *testing.T) {
excluded := []string{"TestContract.f(), TestContract.g()"}
runFuzzerTest(t, &fuzzerSolcFileTest{
filePath: "testdata/contracts/filtering/target_and_exclude.sol",
configUpdates: func(config *config.ProjectConfig) {
config.Fuzzing.TargetContracts = []string{"TestContract"}
config.Fuzzing.Testing.ExcludeFunctionSignatures = excluded
},
method: func(f *fuzzerTestContext) {
for _, contract := range f.fuzzer.ContractDefinitions() {
// Only h and i should be test since f and g are excluded
reflect.DeepEqual(contract.AssertionTestMethods(), []string{"TestContract.h()", "TestContract.i()"})
// ALL properties and optimizations should be tested
reflect.DeepEqual(contract.PropertyTestMethods(), []string{"TestContract.property_a()"})
reflect.DeepEqual(contract.OptimizationTestMethods(), []string{"TestContract.optimize_b()"})
}
}})
}
35 changes: 35 additions & 0 deletions fuzzing/testdata/contracts/filtering/target_and_exclude.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// This contract ensures that we can target or exclude functions
contract TestContract {
uint odd_counter = 1;
uint even_counter = 2;
event Counter(uint256 value);
function f() public {
odd_counter += 1;
emit Counter(odd_counter);
}

function g() public {
even_counter += 2;
emit Counter(even_counter);

}

function h() public {
odd_counter += 3;
emit Counter(odd_counter);

}

function i() public {
even_counter += 4;
emit Counter(even_counter);
}

function property_a() public view returns (bool) {
return (odd_counter != 100);
}

function optimize_b() public view returns (int256) {
return -1;
}
}
7 changes: 4 additions & 3 deletions fuzzing/utils/fuzz_method_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package utils
import (
"strings"

compilationTypes "github.com/crytic/medusa/compilation/types"
"github.com/crytic/medusa/fuzzing/config"
"github.com/ethereum/go-ethereum/accounts/abi"
)
Expand Down Expand Up @@ -42,9 +43,9 @@ func isAssertionTest(method abi.Method, includeViewMethods bool) bool {
return !method.IsConstant() || includeViewMethods
}

// BinTestByType takes a list of methods and returns whether they are assertion, property, or optimization tests.
func BinTestByType(methods []abi.Method, testCfg config.TestingConfig) (assertionTests, propertyTests, optimizationTests []abi.Method) {
for _, method := range methods {
// BinTestByType sorts a contract's methods by whether they are assertion, property, or optimization tests.
func BinTestByType(contract *compilationTypes.CompiledContract, testCfg config.TestingConfig) (assertionTests, propertyTests, optimizationTests []abi.Method) {
for _, method := range contract.Abi.Methods {
if isPropertyTest(method, testCfg.PropertyTesting.TestPrefixes) {
propertyTests = append(propertyTests, method)
} else if isOptimizationTest(method, testCfg.OptimizationTesting.TestPrefixes) {
Expand Down

0 comments on commit 5a66fa2

Please sign in to comment.