diff --git a/docs/src/project_configuration/testing_config.md b/docs/src/project_configuration/testing_config.md index a9e2f073..88f78c0f 100644 --- a/docs/src/project_configuration/testing_config.md +++ b/docs/src/project_configuration/testing_config.md @@ -63,20 +63,6 @@ contract MyContract { that triggered a test failure. - **Default**: `false` -### `targetFunctionSignatures`: - -- **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**: `[]` - ## Assertion Testing Configuration ### `enabled` diff --git a/fuzzing/config/config.go b/fuzzing/config/config.go index e488238c..f6782292 100644 --- a/fuzzing/config/config.go +++ b/fuzzing/config/config.go @@ -141,48 +141,6 @@ type TestingConfig struct { // OptimizationTesting describes the configuration used for optimization testing. OptimizationTesting OptimizationTestingConfig `json:"optimizationTesting"` - - // TargetFunctionSignatures is a list function signatures call 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)`. - TargetFunctionSignatures []string `json:"targetFunctionSignatures"` - - // ExcludeFunctionSignatures is a list of function signatures that will be excluded from call sequences. - // The signatures should specify the contract name and signature in the ABI format like `Contract.func(uint256,bytes32)`. - ExcludeFunctionSignatures []string `json:"excludeFunctionSignatures"` -} - -// Validate validates that the TestingConfig meets certain requirements. -func (testCfg *TestingConfig) Validate() error { - // Verify that target and exclude function signatures are used mutually exclusive. - if (len(testCfg.TargetFunctionSignatures) != 0) && (len(testCfg.ExcludeFunctionSignatures) != 0) { - return errors.New("project configuration must specify only one of blacklist or whitelist at a time") - } - - // Verify property testing fields. - if testCfg.PropertyTesting.Enabled { - // Test prefixes must be supplied if property testing is enabled. - if len(testCfg.PropertyTesting.TestPrefixes) == 0 { - return errors.New("project configuration must specify test name prefixes if property testing is enabled") - } - } - - if testCfg.OptimizationTesting.Enabled { - // Test prefixes must be supplied if optimization testing is enabled. - if len(testCfg.OptimizationTesting.TestPrefixes) == 0 { - return errors.New("project configuration must specify test name prefixes if optimization testing is enabled") - } - } - - // Validate that prefixes do not overlap - for _, prefix := range testCfg.PropertyTesting.TestPrefixes { - for _, prefix2 := range testCfg.OptimizationTesting.TestPrefixes { - if prefix == prefix2 { - return errors.New("project configuration must specify unique test name prefixes for property and optimization testing") - } - } - } - - return nil } // AssertionTestingConfig describes the configuration options used for assertion testing @@ -258,7 +216,7 @@ type LoggingConfig struct { // equivalent to enabling file logging. LogDirectory string `json:"logDirectory"` - // NoColor indicates whether log messages should be displayed with colored formatting. + // NoColor indicates whether or not log messages should be displayed with colored formatting. NoColor bool `json:"noColor"` } @@ -328,11 +286,6 @@ func (p *ProjectConfig) Validate() error { logger = logging.GlobalLogger.NewSubLogger("module", "fuzzer config") } - // Validate testing config - if err := p.Fuzzing.Testing.Validate(); err != nil { - return err - } - // Verify the worker count is a positive number. if p.Fuzzing.Workers <= 0 { return errors.New("project configuration must specify a positive number for the worker count") diff --git a/fuzzing/config/config_defaults.go b/fuzzing/config/config_defaults.go index 10f45dc1..b77b2143 100644 --- a/fuzzing/config/config_defaults.go +++ b/fuzzing/config/config_defaults.go @@ -1,11 +1,10 @@ package config import ( - "math/big" - testChainConfig "github.com/crytic/medusa/chain/config" "github.com/crytic/medusa/compilation" "github.com/rs/zerolog" + "math/big" ) // GetDefaultProjectConfig obtains a default configuration for a project. It populates a default compilation config @@ -62,8 +61,6 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) { StopOnNoTests: true, TestAllContracts: false, TraceAll: false, - TargetFunctionSignatures: []string{}, - ExcludeFunctionSignatures: []string{}, AssertionTesting: AssertionTestingConfig{ Enabled: true, TestViewMethods: false, diff --git a/fuzzing/contracts/contract.go b/fuzzing/contracts/contract.go index 30ad094a..17ec9f66 100644 --- a/fuzzing/contracts/contract.go +++ b/fuzzing/contracts/contract.go @@ -1,11 +1,7 @@ package contracts import ( - "golang.org/x/exp/slices" - "strings" - "github.com/crytic/medusa/compilation/types" - "github.com/ethereum/go-ethereum/accounts/abi" ) // Contracts describes an array of contracts @@ -39,17 +35,6 @@ type Contract struct { // compilation describes the compilation which contains the compiledContract. compilation *types.Compilation - - // PropertyTestMethods are the methods that are property tests. - PropertyTestMethods []abi.Method - - // OptimizationTestMethods are the methods that are optimization tests. - OptimizationTestMethods []abi.Method - - // 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. @@ -62,32 +47,6 @@ func NewContract(name string, sourcePath string, compiledContract *types.Compile } } -// WithTargetedAssertionMethods filters the assertion test methods to those in the target list. -func (c *Contract) WithTargetedAssertionMethods(target []string) *Contract { - var candidateMethods []abi.Method - for _, method := range c.AssertionTestMethods { - canonicalSig := strings.Join([]string{c.name, method.Sig}, ".") - if slices.Contains(target, canonicalSig) { - candidateMethods = append(candidateMethods, method) - } - } - c.AssertionTestMethods = candidateMethods - return c -} - -// WithExcludedAssertionMethods filters the assertion test methods to all methods not in excluded list. -func (c *Contract) WithExcludedAssertionMethods(excludedMethods []string) *Contract { - var candidateMethods []abi.Method - for _, method := range c.AssertionTestMethods { - canonicalSig := strings.Join([]string{c.name, method.Sig}, ".") - if !slices.Contains(excludedMethods, canonicalSig) { - candidateMethods = append(candidateMethods, method) - } - } - c.AssertionTestMethods = candidateMethods - return c -} - // Name returns the name of the contract. func (c *Contract) Name() string { return c.name diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index 960ebfe2..18a0ed0f 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -32,7 +32,6 @@ import ( "github.com/crytic/medusa/fuzzing/config" fuzzerTypes "github.com/crytic/medusa/fuzzing/contracts" "github.com/crytic/medusa/fuzzing/corpus" - fuzzingutils "github.com/crytic/medusa/fuzzing/utils" "github.com/crytic/medusa/fuzzing/valuegeneration" "github.com/crytic/medusa/utils" "github.com/ethereum/go-ethereum/accounts/abi" @@ -283,7 +282,7 @@ 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 + // Loop for each contract in each compilation and deploy it to the test node. 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]) @@ -298,26 +297,6 @@ 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.PropertyTesting.TestPrefixes, - f.config.Fuzzing.Testing.OptimizationTesting.TestPrefixes, - f.config.Fuzzing.Testing.AssertionTesting.TestViewMethods) - contractDefinition.AssertionTestMethods = assertionTestMethods - contractDefinition.PropertyTestMethods = propertyTestMethods - contractDefinition.OptimizationTestMethods = optimizationTestMethods - - // Filter and record methods available for assertion testing. Property and optimization tests are always run. - if len(f.config.Fuzzing.Testing.TargetFunctionSignatures) > 0 { - // Only consider methods that are in the target methods list - contractDefinition = contractDefinition.WithTargetedAssertionMethods(f.config.Fuzzing.Testing.TargetFunctionSignatures) - } - if len(f.config.Fuzzing.Testing.ExcludeFunctionSignatures) > 0 { - // Consider all methods except those in the exclude methods list - contractDefinition = contractDefinition.WithExcludedAssertionMethods(f.config.Fuzzing.Testing.ExcludeFunctionSignatures) - } - f.contractDefinitions = append(f.contractDefinitions, contractDefinition) } } @@ -394,7 +373,6 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) (*ex // Verify that target contracts is not empty. If it's empty, but we only have one contract definition, // we can infer the target contracts. Otherwise, we report an error. if len(fuzzer.config.Fuzzing.TargetContracts) == 0 { - // TODO filter libraries if len(fuzzer.contractDefinitions) == 1 { fuzzer.config.Fuzzing.TargetContracts = []string{fuzzer.contractDefinitions[0].Name()} } else { diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 0af56dd9..1898e578 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -4,7 +4,6 @@ import ( "encoding/hex" "math/big" "math/rand" - "reflect" "testing" "github.com/crytic/medusa/fuzzing/executiontracer" @@ -894,45 +893,3 @@ func TestDeploymentOrderWithCoverage(t *testing.T) { }, }) } - -// TestTargetingFuncSignatures tests whether functions will be correctly whitelisted for testing -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) - - // ALL properties and optimizations should be tested - reflect.DeepEqual(contract.PropertyTestMethods, []string{"TestContract.property_a()"}) - reflect.DeepEqual(contract.OptimizationTestMethods, []string{"TestContract.optimize_b()"}) - } - }}) -} - -// TestExcludeFunctionSignatures tests whether functions will be blacklisted/excluded for testing -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()"}) - } - }}) -} diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index 7ac958b2..ae4b9cc8 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -90,7 +90,6 @@ func newFuzzerWorker(fuzzer *Fuzzer, workerIndex int, randomProvider *rand.Rand) fuzzer: fuzzer, deployedContracts: make(map[common.Address]*fuzzerTypes.Contract), stateChangingMethods: make([]fuzzerTypes.DeployedContractMethod, 0), - pureMethods: make([]fuzzerTypes.DeployedContractMethod, 0), coverageTracer: nil, randomProvider: randomProvider, valueSet: valueSet, @@ -240,9 +239,9 @@ func (fw *FuzzerWorker) updateMethods() { // Loop through each deployed contract for contractAddress, contractDefinition := range fw.deployedContracts { // If we deployed the contract, also enumerate property tests and state changing methods. - for _, method := range contractDefinition.AssertionTestMethods { + for _, method := range contractDefinition.CompiledContract().Abi.Methods { // Any non-constant method should be tracked as a state changing method. - // We favor calling state changing methods over view/pure methods. + // We favor calling state changing methods over view methods. if method.IsConstant() { fw.pureMethods = append(fw.pureMethods, fuzzerTypes.DeployedContractMethod{Address: contractAddress, Contract: contractDefinition, Method: method}) fw.methodChooser.AddChoices(randomutils.NewWeightedRandomChoice(fuzzerTypes.DeployedContractMethod{Address: contractAddress, Contract: contractDefinition, Method: method}, big.NewInt(1))) diff --git a/fuzzing/test_case_assertion_provider.go b/fuzzing/test_case_assertion_provider.go index 8ab4a5bd..233e2ef3 100644 --- a/fuzzing/test_case_assertion_provider.go +++ b/fuzzing/test_case_assertion_provider.go @@ -7,6 +7,8 @@ import ( "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/config" "github.com/crytic/medusa/fuzzing/contracts" + "github.com/crytic/medusa/fuzzing/utils" + "github.com/ethereum/go-ethereum/accounts/abi" "golang.org/x/exp/slices" ) @@ -42,6 +44,23 @@ func attachAssertionTestCaseProvider(fuzzer *Fuzzer) *AssertionTestCaseProvider return t } +// isTestableMethod checks whether the method is configured by the attached fuzzer to be a target of assertion testing. +// Returns true if this target should be tested, false otherwise. +func (t *AssertionTestCaseProvider) isTestableMethod(method abi.Method) bool { + // Do not test optimization tests + if utils.IsOptimizationTest(method, t.fuzzer.config.Fuzzing.Testing.OptimizationTesting.TestPrefixes) { + return false + } + + // Do not test property tests + if utils.IsPropertyTest(method, t.fuzzer.config.Fuzzing.Testing.PropertyTesting.TestPrefixes) { + return false + } + + // Only test constant methods (pure/view) if we are configured to. + return !method.IsConstant() || t.fuzzer.config.Fuzzing.Testing.AssertionTesting.TestViewMethods +} + // checkAssertionFailures checks the results of the last call for assertion failures. // Returns the method ID, a boolean indicating if an assertion test failed, or an error if one occurs. func (t *AssertionTestCaseProvider) checkAssertionFailures(callSequence calls.CallSequence) (*contracts.ContractMethodID, bool, error) { @@ -86,7 +105,12 @@ func (t *AssertionTestCaseProvider) onFuzzerStarting(event FuzzerStartingEvent) continue } - for _, method := range contract.AssertionTestMethods { + for _, method := range contract.CompiledContract().Abi.Methods { + // Verify this method is an assertion testable method + if !t.isTestableMethod(method) { + continue + } + // Create local variables to avoid pointer types in the loop being overridden. contract := contract method := method diff --git a/fuzzing/test_case_optimization_provider.go b/fuzzing/test_case_optimization_provider.go index e782c646..2177e39b 100644 --- a/fuzzing/test_case_optimization_provider.go +++ b/fuzzing/test_case_optimization_provider.go @@ -8,6 +8,7 @@ import ( "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/contracts" "github.com/crytic/medusa/fuzzing/executiontracer" + "github.com/crytic/medusa/fuzzing/utils" "github.com/ethereum/go-ethereum/core" "golang.org/x/exp/slices" ) @@ -134,7 +135,11 @@ func (t *OptimizationTestCaseProvider) onFuzzerStarting(event FuzzerStartingEven continue } - for _, method := range contract.OptimizationTestMethods { + for _, method := range contract.CompiledContract().Abi.Methods { + // Verify this method is an optimization test method + if !utils.IsOptimizationTest(method, t.fuzzer.config.Fuzzing.Testing.OptimizationTesting.TestPrefixes) { + continue + } // Create local variables to avoid pointer types in the loop being overridden. contract := contract method := method diff --git a/fuzzing/test_case_property_provider.go b/fuzzing/test_case_property_provider.go index 3681d218..db990cbc 100644 --- a/fuzzing/test_case_property_provider.go +++ b/fuzzing/test_case_property_provider.go @@ -8,6 +8,7 @@ import ( "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/contracts" "github.com/crytic/medusa/fuzzing/executiontracer" + "github.com/crytic/medusa/fuzzing/utils" "github.com/ethereum/go-ethereum/core" "golang.org/x/exp/slices" ) @@ -136,7 +137,12 @@ func (t *PropertyTestCaseProvider) onFuzzerStarting(event FuzzerStartingEvent) e continue } - for _, method := range contract.PropertyTestMethods { + for _, method := range contract.CompiledContract().Abi.Methods { + // Verify this method is a property test method + if !utils.IsPropertyTest(method, t.fuzzer.config.Fuzzing.Testing.PropertyTesting.TestPrefixes) { + continue + } + // Create local variables to avoid pointer types in the loop being overridden. contract := contract method := method diff --git a/fuzzing/testdata/contracts/deployments/deploy_payable_constructors.sol b/fuzzing/testdata/contracts/deployments/deploy_payable_constructors.sol index f976e48f..223d85c4 100644 --- a/fuzzing/testdata/contracts/deployments/deploy_payable_constructors.sol +++ b/fuzzing/testdata/contracts/deployments/deploy_payable_constructors.sol @@ -6,9 +6,6 @@ contract FirstContract { function property_contract_has_no_balance() public returns(bool) { return address(this).balance == 0; } - - // This exists so the fuzzer knows there are state changing methods to target, instead of quitting early. - function dummy() public {} } @@ -18,8 +15,4 @@ contract SecondContract { function property_contract_has_balance() public returns(bool) { return address(this).balance == 1 ether; } - - // This exists so the fuzzer knows there are state changing methods to target, instead of quitting early. - function dummy() public {} - } diff --git a/fuzzing/testdata/contracts/filtering/target_and_exclude.sol b/fuzzing/testdata/contracts/filtering/target_and_exclude.sol deleted file mode 100644 index 65c76dce..00000000 --- a/fuzzing/testdata/contracts/filtering/target_and_exclude.sol +++ /dev/null @@ -1,35 +0,0 @@ -// 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; - } -} diff --git a/fuzzing/utils/fuzz_method_utils.go b/fuzzing/utils/fuzz_method_utils.go index 70b77a12..1174246b 100644 --- a/fuzzing/utils/fuzz_method_utils.go +++ b/fuzzing/utils/fuzz_method_utils.go @@ -1,10 +1,8 @@ package utils import ( - "strings" - - compilationTypes "github.com/crytic/medusa/compilation/types" "github.com/ethereum/go-ethereum/accounts/abi" + "strings" ) // IsOptimizationTest checks whether the method is an optimization test given potential naming prefixes it must conform to @@ -27,26 +25,10 @@ func IsOptimizationTest(method abi.Method, prefixes []string) bool { func IsPropertyTest(method abi.Method, prefixes []string) bool { // Loop through all enabled prefixes to find a match for _, prefix := range prefixes { - // The property test must simply have the right prefix and take no inputs and return a boolean - if strings.HasPrefix(method.Name, prefix) { - if len(method.Inputs) == 0 && len(method.Outputs) == 1 && method.Outputs[0].Type.T == abi.BoolTy { - return true - } + // The property test must simply have the right prefix and take no inputs + if strings.HasPrefix(method.Name, prefix) && len(method.Inputs) == 0 { + return true } } return false } - -// BinTestByType sorts a contract's methods by whether they are assertion, property, or optimization tests. -func BinTestByType(contract *compilationTypes.CompiledContract, propertyTestPrefixes, optimizationTestPrefixes []string, testViewMethods bool) (assertionTests, propertyTests, optimizationTests []abi.Method) { - for _, method := range contract.Abi.Methods { - if IsPropertyTest(method, propertyTestPrefixes) { - propertyTests = append(propertyTests, method) - } else if IsOptimizationTest(method, optimizationTestPrefixes) { - optimizationTests = append(optimizationTests, method) - } else if !method.IsConstant() || testViewMethods { - assertionTests = append(assertionTests, method) - } - } - return assertionTests, propertyTests, optimizationTests -}