From c444be91589a3104f0ac9cb29a3c7143334e260c Mon Sep 17 00:00:00 2001 From: Tarun Bansal Date: Wed, 21 Jun 2023 22:39:21 +0530 Subject: [PATCH] Add support for optimization mode (#75) integrate optimization mode into fuzzer --- fuzzing/config/config.go | 12 + fuzzing/config/config_defaults.go | 6 + fuzzing/fuzzer.go | 5 + fuzzing/fuzzer_test.go | 34 ++ fuzzing/test_case_assertion.go | 10 +- fuzzing/test_case_assertion_provider.go | 2 +- fuzzing/test_case_optimization.go | 77 ++++ fuzzing/test_case_optimization_provider.go | 373 ++++++++++++++++++ fuzzing/test_case_property.go | 13 +- fuzzing/test_case_property_provider.go | 4 +- .../contracts/optimizations/optimize.sol | 14 + 11 files changed, 540 insertions(+), 10 deletions(-) create mode 100644 fuzzing/test_case_optimization.go create mode 100644 fuzzing/test_case_optimization_provider.go create mode 100644 fuzzing/testdata/contracts/optimizations/optimize.sol diff --git a/fuzzing/config/config.go b/fuzzing/config/config.go index 72e04acf..f6df3aad 100644 --- a/fuzzing/config/config.go +++ b/fuzzing/config/config.go @@ -104,6 +104,9 @@ type TestingConfig struct { // PropertyTesting describes the configuration used for property testing. PropertyTesting PropertyTestConfig `json:"propertyTesting"` + + // OptimizationTesting describes the configuration used for optimization testing. + OptimizationTesting OptimizationTestingConfig `json:"optimizationTesting"` } // AssertionTestingConfig describes the configuration options used for assertion testing @@ -124,6 +127,15 @@ type PropertyTestConfig struct { TestPrefixes []string `json:"testPrefixes"` } +// OptimizationTestingConfig describes the configuration options used for optimization testing +type OptimizationTestingConfig struct { + // Enabled describes whether testing is enabled. + Enabled bool `json:"enabled"` + + // TestPrefixes dictates what method name prefixes will determine if a contract method is an optimization test. + TestPrefixes []string `json:"testPrefixes"` +} + // ReadProjectConfigFromFile reads a JSON-serialized ProjectConfig from a provided file path. // Returns the ProjectConfig if it succeeds, or an error if one occurs. func ReadProjectConfigFromFile(path string) (*ProjectConfig, error) { diff --git a/fuzzing/config/config_defaults.go b/fuzzing/config/config_defaults.go index cdcf86af..45957f69 100644 --- a/fuzzing/config/config_defaults.go +++ b/fuzzing/config/config_defaults.go @@ -65,6 +65,12 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) { "fuzz_", }, }, + OptimizationTesting: OptimizationTestingConfig{ + Enabled: false, + TestPrefixes: []string{ + "optimize_", + }, + }, }, TestChainConfig: *chainConfig, }, diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index 6a083161..9e7dabc2 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -141,6 +141,11 @@ func NewFuzzer(config config.ProjectConfig) (*Fuzzer, error) { if fuzzer.config.Fuzzing.Testing.AssertionTesting.Enabled { attachAssertionTestCaseProvider(fuzzer) } + if fuzzer.config.Fuzzing.Testing.OptimizationTesting.Enabled { + // TODO: Make this is a warning in the logging PR + fmt.Printf("warning: currently optimization mode's call sequence shrinking is inefficient. this may lead to minor performance issues") + attachOptimizationTestCaseProvider(fuzzer) + } return fuzzer, nil } diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index a1b7aade..66d5d763 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -6,6 +6,7 @@ import ( "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/valuegeneration" "github.com/crytic/medusa/utils" + "math/big" "math/rand" "testing" @@ -127,6 +128,39 @@ func TestAssertionsAndProperties(t *testing.T) { }) } +// TestOptimizationsSolving runs a test to ensure that optimization mode works as expected +func TestOptimizationsSolving(t *testing.T) { + filePaths := []string{ + "testdata/contracts/optimizations/optimize.sol", + } + for _, filePath := range filePaths { + runFuzzerTest(t, &fuzzerSolcFileTest{ + filePath: filePath, + configUpdates: func(config *config.ProjectConfig) { + config.Fuzzing.DeploymentOrder = []string{"TestContract"} + config.Fuzzing.Testing.PropertyTesting.Enabled = false + config.Fuzzing.Testing.AssertionTesting.Enabled = false + config.Fuzzing.Testing.OptimizationTesting.Enabled = true + config.Fuzzing.TestLimit = 1_000 // this test should expose a failure quickly. + }, + method: func(f *fuzzerTestContext) { + // Start the fuzzer + err := f.fuzzer.Start() + assert.NoError(t, err) + + // Check the value found for optimization test + var testCases = f.fuzzer.TestCasesWithStatus(TestCaseStatusPassed) + switch v := testCases[0].(type) { + case *OptimizationTestCase: + assert.EqualValues(t, v.Value().Cmp(big.NewInt(4241)), 0) + default: + t.Errorf("invalid test case found %T", v) + } + }, + }) + } +} + // TestChainBehaviour runs tests to ensure the chain behaves as expected. func TestChainBehaviour(t *testing.T) { // Run a test to simulate out of gas errors to make sure its handled well by the Chain and does not panic. diff --git a/fuzzing/test_case_assertion.go b/fuzzing/test_case_assertion.go index 8ea42eb0..354c2161 100644 --- a/fuzzing/test_case_assertion.go +++ b/fuzzing/test_case_assertion.go @@ -11,10 +11,14 @@ import ( // AssertionTestCase describes a test being run by a AssertionTestCaseProvider. type AssertionTestCase struct { - status TestCaseStatus + // status describes the status of the test case + status TestCaseStatus + // targetContract describes the target contract where the test case was found targetContract *fuzzerTypes.Contract - targetMethod abi.Method - callSequence *calls.CallSequence + // targetMethod describes the target method for the test case + targetMethod abi.Method + // callSequence describes the call sequence that broke the assertion + callSequence *calls.CallSequence } // Status describes the TestCaseStatus used to define the current state of the test. diff --git a/fuzzing/test_case_assertion_provider.go b/fuzzing/test_case_assertion_provider.go index 35f0d7e6..e332cd65 100644 --- a/fuzzing/test_case_assertion_provider.go +++ b/fuzzing/test_case_assertion_provider.go @@ -118,7 +118,7 @@ func (t *AssertionTestCaseProvider) onFuzzerStarting(event FuzzerStartingEvent) return nil } -// onFuzzerStarting is the event handler triggered when the Fuzzer is stopping the fuzzing campaign and all workers +// onFuzzerStopping is the event handler triggered when the Fuzzer is stopping the fuzzing campaign and all workers // have been destroyed. It clears state tracked for each FuzzerWorker and sets test cases in "running" states to // "passed". func (t *AssertionTestCaseProvider) onFuzzerStopping(event FuzzerStoppingEvent) error { diff --git a/fuzzing/test_case_optimization.go b/fuzzing/test_case_optimization.go new file mode 100644 index 00000000..3110c0ad --- /dev/null +++ b/fuzzing/test_case_optimization.go @@ -0,0 +1,77 @@ +package fuzzing + +import ( + "fmt" + "github.com/crytic/medusa/fuzzing/calls" + "github.com/crytic/medusa/fuzzing/contracts" + "github.com/crytic/medusa/fuzzing/executiontracer" + "github.com/ethereum/go-ethereum/accounts/abi" + "math/big" + "strings" + "sync" +) + +// OptimizationTestCase describes a test being run by a OptimizationTestCaseProvider. +type OptimizationTestCase struct { + // status describes the status of the test case + status TestCaseStatus + // targetContract describes the target contract where the test case was found + targetContract *contracts.Contract + // targetMethod describes the target method for the test case + targetMethod abi.Method + // callSequence describes the call sequence that maximized the value + callSequence *calls.CallSequence + // value is used to store the maximum value returned by the test method + value *big.Int + // valueLock is used for thread-synchronization when updating the value + valueLock sync.Mutex + // optimizationTestTrace describes the execution trace when running the callSequence + optimizationTestTrace *executiontracer.ExecutionTrace +} + +// Status describes the TestCaseStatus used to define the current state of the test. +func (t *OptimizationTestCase) Status() TestCaseStatus { + return t.status +} + +// CallSequence describes the calls.CallSequence of calls sent to the EVM which resulted in this TestCase result. +// This should be nil if the result is not related to the CallSequence. +func (t *OptimizationTestCase) CallSequence() *calls.CallSequence { + return t.callSequence +} + +// Name describes the name of the test case. +func (t *OptimizationTestCase) Name() string { + return fmt.Sprintf("Optimization Test: %s.%s", t.targetContract.Name(), t.targetMethod.Sig) +} + +// Message obtains a text-based printable message which describes the test result. +func (t *OptimizationTestCase) Message() string { + // We print final value in case the test case passed for optimization test + if t.Status() == TestCaseStatusPassed { + msg := fmt.Sprintf( + "Optimization test \"%s.%s\" resulted in the maximum value: %s with the following sequence:\n%s", + t.targetContract.Name(), + t.targetMethod.Sig, + t.value, + t.CallSequence().String(), + ) + // If an execution trace is attached then add it to the message + if t.optimizationTestTrace != nil { + // TODO: Improve formatting in logging PR + msg += fmt.Sprintf("\nOptimization test execution trace:\n%s", t.optimizationTestTrace.String()) + } + return msg + } + return "" +} + +// ID obtains a unique identifier for a test result. +func (t *OptimizationTestCase) ID() string { + return strings.Replace(fmt.Sprintf("OPTIMIZATION-%s-%s", t.targetContract.Name(), t.targetMethod.Sig), "_", "-", -1) +} + +// Value obtains the maximum value returned by the test method found till now +func (t *OptimizationTestCase) Value() *big.Int { + return t.value +} diff --git a/fuzzing/test_case_optimization_provider.go b/fuzzing/test_case_optimization_provider.go new file mode 100644 index 00000000..622aa155 --- /dev/null +++ b/fuzzing/test_case_optimization_provider.go @@ -0,0 +1,373 @@ +package fuzzing + +import ( + "fmt" + "github.com/crytic/medusa/fuzzing/calls" + "github.com/crytic/medusa/fuzzing/contracts" + "github.com/crytic/medusa/fuzzing/executiontracer" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/core" + "math/big" + "strings" + "sync" +) + +const MIN_INT = "-8000000000000000000000000000000000000000000000000000000000000000" + +// OptimizationTestCaseProvider is a provider for on-chain optimization tests. +// Optimization tests are represented as publicly-accessible functions which have a name prefix specified by a +// config.FuzzingConfig. They take no input arguments and return an integer value that needs to be maximized. +type OptimizationTestCaseProvider struct { + // fuzzer describes the Fuzzer which this provider is attached to. + fuzzer *Fuzzer + + // testCases is a map of contract-method IDs to optimization test cases.GetContractMethodID + testCases map[contracts.ContractMethodID]*OptimizationTestCase + + // testCasesLock is used for thread-synchronization when updating testCases + testCasesLock sync.Mutex + + // workerStates is a slice where each element stores state for a given worker index. + workerStates []optimizationTestCaseProviderWorkerState +} + +// optimizationTestCaseProviderWorkerState represents the state for an individual worker maintained by +// OptimizationTestCaseProvider. +type optimizationTestCaseProviderWorkerState struct { + // optimizationTestMethods a mapping from contract-method ID to deployed contract-method descriptors. + // Each deployed contract-method represents an optimization test method to call for evaluation. Optimization tests + // should be read-only functions which take no input parameters and return an integer variable. + optimizationTestMethods map[contracts.ContractMethodID]contracts.DeployedContractMethod + + // optimizationTestMethodsLock is used for thread-synchronization when updating optimizationTestMethods + optimizationTestMethodsLock sync.Mutex +} + +// attachOptimizationTestCaseProvider attaches a new OptimizationTestCaseProvider to the Fuzzer and returns it. +func attachOptimizationTestCaseProvider(fuzzer *Fuzzer) *OptimizationTestCaseProvider { + // Create a test case provider + t := &OptimizationTestCaseProvider{ + fuzzer: fuzzer, + } + + // Subscribe the provider to relevant events the fuzzer emits. + fuzzer.Events.FuzzerStarting.Subscribe(t.onFuzzerStarting) + fuzzer.Events.FuzzerStopping.Subscribe(t.onFuzzerStopping) + fuzzer.Events.WorkerCreated.Subscribe(t.onWorkerCreated) + + // Add the provider's call sequence test function to the fuzzer. + fuzzer.Hooks.CallSequenceTestFuncs = append(fuzzer.Hooks.CallSequenceTestFuncs, t.callSequencePostCallTest) + return t +} + +// isOptimizationTest check whether the method is an optimization test given potential naming prefixes it must conform to +// and its underlying input/output arguments. +func (t *OptimizationTestCaseProvider) isOptimizationTest(method abi.Method) bool { + // Loop through all enabled prefixes to find a match + for _, prefix := range t.fuzzer.Config().Fuzzing.Testing.OptimizationTesting.TestPrefixes { + if strings.HasPrefix(method.Name, prefix) { + // An optimization test must take no inputs and return an int256 + if len(method.Inputs) == 0 && len(method.Outputs) == 1 && method.Outputs[0].Type.T == abi.IntTy && method.Outputs[0].Type.Size == 256 { + return true + } + } + } + return false +} + +// runOptimizationTest executes a given optimization test method (w/ an optional execution trace) and returns the return value +// from the optimization test method. This is called after every call the Fuzzer makes when testing call sequences for each test case. +func (t *OptimizationTestCaseProvider) runOptimizationTest(worker *FuzzerWorker, optimizationTestMethod *contracts.DeployedContractMethod, trace bool) (*big.Int, *executiontracer.ExecutionTrace, error) { + // Generate our ABI input data for the call. In this case, optimization test methods take no arguments, so the + // variadic argument list here is empty. + data, err := optimizationTestMethod.Contract.CompiledContract().Abi.Pack(optimizationTestMethod.Method.Name) + if err != nil { + return nil, nil, err + } + + // Call the underlying contract + value := big.NewInt(0) + // TODO: Determine if we should use `Senders[0]` or have a separate funded account for the optimizations. + msg := calls.NewCallMessage(worker.Fuzzer().senders[0], &optimizationTestMethod.Address, 0, value, worker.fuzzer.config.Fuzzing.TransactionGasLimit, nil, nil, nil, data) + msg.FillFromTestChainProperties(worker.chain) + + // Execute the call. If we are tracing, we attach an execution tracer and obtain the result. + var executionResult *core.ExecutionResult + var executionTrace *executiontracer.ExecutionTrace + if trace { + executionTracer := executiontracer.NewExecutionTracer(worker.fuzzer.contractDefinitions, worker.chain.CheatCodeContracts()) + executionResult, err = worker.Chain().CallContract(msg, nil, executionTracer) + executionTrace = executionTracer.Trace() + } else { + executionResult, err = worker.Chain().CallContract(msg, nil) + } + if err != nil { + return nil, nil, fmt.Errorf("failed to call optimization test method: %v", err) + } + + // If the execution reverted, then we know that we do not have any valuable return data, so we return the smallest + // integer value + if executionResult.Failed() { + minInt256, _ := new(big.Int).SetString(MIN_INT, 16) + return minInt256, nil, nil + } + + // Decode our ABI outputs + retVals, err := optimizationTestMethod.Method.Outputs.Unpack(executionResult.Return()) + if err != nil { + return nil, nil, fmt.Errorf("failed to decode optimization test method return value: %v", err) + } + + // We should have one return value. + if len(retVals) != 1 { + return nil, nil, fmt.Errorf("detected an unexpected number of return values from optimization test '%s'", optimizationTestMethod.Method.Name) + } + + // Parse the return value and it should be an int256 + newValue, ok := retVals[0].(*big.Int) + if !ok { + return nil, nil, fmt.Errorf("failed to parse optimization test's: %s return value: %v", optimizationTestMethod.Method.Name, retVals[0]) + } + + return newValue, executionTrace, nil +} + +// onFuzzerStarting is the event handler triggered when the Fuzzer is starting a fuzzing campaign. It creates test cases +// in a "not started" state for every optimization test method discovered in the contract definitions known to the Fuzzer. +func (t *OptimizationTestCaseProvider) onFuzzerStarting(event FuzzerStartingEvent) error { + // Reset our state + t.testCases = make(map[contracts.ContractMethodID]*OptimizationTestCase) + t.workerStates = make([]optimizationTestCaseProviderWorkerState, t.fuzzer.Config().Fuzzing.Workers) + + // Create a test case for every optimization test method. + for _, contract := range t.fuzzer.ContractDefinitions() { + for _, method := range contract.CompiledContract().Abi.Methods { + // Verify this method is an optimization test method + if !t.isOptimizationTest(method) { + continue + } + // Create local variables to avoid pointer types in the loop being overridden. + contract := contract + method := method + minInt256, _ := new(big.Int).SetString(MIN_INT, 16) + + // Create our optimization test case + optimizationTestCase := &OptimizationTestCase{ + status: TestCaseStatusNotStarted, + targetContract: contract, + targetMethod: method, + callSequence: nil, + value: minInt256, + } + + // Add to our test cases and register them with the fuzzer + methodId := contracts.GetContractMethodID(contract, &method) + t.testCases[methodId] = optimizationTestCase + t.fuzzer.RegisterTestCase(optimizationTestCase) + } + } + return nil +} + +// onFuzzerStopping is the event handler triggered when the Fuzzer is stopping the fuzzing campaign and all workers +// have been destroyed. It clears state tracked for each FuzzerWorker and sets test cases in "running" states to +// "passed". +func (t *OptimizationTestCaseProvider) onFuzzerStopping(event FuzzerStoppingEvent) error { + // Clear our optimization test methods + t.workerStates = nil + + // Loop through each test case and set any tests with a running status to a passed status. + for _, testCase := range t.testCases { + if testCase.status == TestCaseStatusRunning { + testCase.status = TestCaseStatusPassed + // Since optimization tests do not really "finish", we will report that they are finished when the fuzzer + // stops. + if event.Fuzzer != nil { + event.Fuzzer.ReportTestCaseFinished(testCase) + } + } + } + return nil +} + +// onWorkerCreated is the event handler triggered when a FuzzerWorker is created by the Fuzzer. It ensures state tracked +// for that worker index is refreshed and subscribes to relevant worker events. +func (t *OptimizationTestCaseProvider) onWorkerCreated(event FuzzerWorkerCreatedEvent) error { + // Create a new state for this worker. + t.workerStates[event.Worker.WorkerIndex()] = optimizationTestCaseProviderWorkerState{ + optimizationTestMethods: make(map[contracts.ContractMethodID]contracts.DeployedContractMethod), + optimizationTestMethodsLock: sync.Mutex{}, + } + + // Subscribe to relevant worker events. + event.Worker.Events.ContractAdded.Subscribe(t.onWorkerDeployedContractAdded) + event.Worker.Events.ContractDeleted.Subscribe(t.onWorkerDeployedContractDeleted) + return nil +} + +// onWorkerDeployedContractAdded is the event handler triggered when a FuzzerWorker detects a new contract deployment +// on its underlying chain. It ensures any optimization test methods which the deployed contract contains are tracked by the +// provider for testing. Any test cases previously made for these methods which are in a "not started" state are put +// into a "running" state, as they are now potentially reachable for testing. +func (t *OptimizationTestCaseProvider) onWorkerDeployedContractAdded(event FuzzerWorkerContractAddedEvent) error { + // If we don't have a contract definition, we can't run optimization tests against the contract. + if event.ContractDefinition == nil { + return nil + } + + // Loop through all methods and find ones for which we have tests + for _, method := range event.ContractDefinition.CompiledContract().Abi.Methods { + // Obtain an identifier for this pair + methodId := contracts.GetContractMethodID(event.ContractDefinition, &method) + + // If we have a test case targeting this contract/method that has not failed, track this deployed method in + // our map for this worker. If we have any tests in a not-started state, we can signal a running state now. + t.testCasesLock.Lock() + optimizationTestCase, optimizationTestCaseExists := t.testCases[methodId] + t.testCasesLock.Unlock() + + if optimizationTestCaseExists { + if optimizationTestCase.Status() == TestCaseStatusNotStarted { + optimizationTestCase.status = TestCaseStatusRunning + } + if optimizationTestCase.Status() != TestCaseStatusFailed { + // Create our optimization test method reference. + workerState := &t.workerStates[event.Worker.WorkerIndex()] + workerState.optimizationTestMethodsLock.Lock() + workerState.optimizationTestMethods[methodId] = contracts.DeployedContractMethod{ + Address: event.ContractAddress, + Contract: event.ContractDefinition, + Method: method, + } + workerState.optimizationTestMethodsLock.Unlock() + } + } + } + return nil +} + +// onWorkerDeployedContractDeleted is the event handler triggered when a FuzzerWorker detects that a previously deployed +// contract no longer exists on its underlying chain. It ensures any optimization test methods which the deployed contract +// contained are no longer tracked by the provider for testing. +func (t *OptimizationTestCaseProvider) onWorkerDeployedContractDeleted(event FuzzerWorkerContractDeletedEvent) error { + // If we don't have a contract definition, there's nothing to do. + if event.ContractDefinition == nil { + return nil + } + + // Loop through all methods and find ones for which we have tests + for _, method := range event.ContractDefinition.CompiledContract().Abi.Methods { + // Obtain an identifier for this pair + methodId := contracts.GetContractMethodID(event.ContractDefinition, &method) + + // If this identifier is in our test cases map, then we remove it from our optimization test method lookup for + // this worker index. + t.testCasesLock.Lock() + _, isOptimizationTestMethod := t.testCases[methodId] + t.testCasesLock.Unlock() + + if isOptimizationTestMethod { + // Delete our optimization test method reference. + workerState := &t.workerStates[event.Worker.WorkerIndex()] + workerState.optimizationTestMethodsLock.Lock() + delete(workerState.optimizationTestMethods, methodId) + workerState.optimizationTestMethodsLock.Unlock() + } + } + return nil +} + +// callSequencePostCallTest provides is a CallSequenceTestFunc that performs post-call testing logic for the attached Fuzzer +// and any underlying FuzzerWorker. It is called after every call made in a call sequence. It checks whether any +// optimization test's value has increased. +func (t *OptimizationTestCaseProvider) callSequencePostCallTest(worker *FuzzerWorker, callSequence calls.CallSequence) ([]ShrinkCallSequenceRequest, error) { + // Create a list of shrink call sequence verifiers, which we populate for each maximized optimization test we want a call + // sequence shrunk for. + shrinkRequests := make([]ShrinkCallSequenceRequest, 0) + + // Obtain the test provider state for this worker + workerState := &t.workerStates[worker.WorkerIndex()] + + // Loop through all optimization test methods and test them. + for optimizationTestMethodId, workerOptimizationTestMethod := range workerState.optimizationTestMethods { + // Obtain the test case for this optimization test method + t.testCasesLock.Lock() + testCase := t.testCases[optimizationTestMethodId] + t.testCasesLock.Unlock() + + // Run our optimization test (create a local copy to avoid loop overwriting the method) + workerOptimizationTestMethod := workerOptimizationTestMethod + newValue, _, err := t.runOptimizationTest(worker, &workerOptimizationTestMethod, false) + if err != nil { + return nil, err + } + + // If we updated the test case's maximum value, we update our state immediately. We provide a shrink verifier which will update + // the call sequence for each shrunken sequence provided that still it maintains the maximum value. + // TODO: This is very inefficient since this runs every time a new max value is found. It would be ideal if we + // could perform a one-time shrink request. This code should be refactored when we introduce the high-level + // testing API. + if newValue.Cmp(testCase.value) == 1 { + // Create a request to shrink this call sequence. + shrinkRequest := ShrinkCallSequenceRequest{ + VerifierFunction: func(worker *FuzzerWorker, shrunkenCallSequence calls.CallSequence) (bool, error) { + // First verify the contract to the optimization test is still deployed to call upon. + _, optimizationTestContractDeployed := worker.deployedContracts[workerOptimizationTestMethod.Address] + if !optimizationTestContractDeployed { + // If the contract isn't available, this shrunk sequence likely messed up deployment, so we + // report it as an invalid solution. + return false, nil + } + + // Then the shrink verifier ensures that the maximum value has either stayed the same or, hopefully, + // increased. + shrunkenSequenceNewValue, _, err := t.runOptimizationTest(worker, &workerOptimizationTestMethod, false) + + // If the shrunken value is greater than new value, then set new value to the shrunken one so that it + // can be tracked correctly in the finished callback + if err == nil && shrunkenSequenceNewValue.Cmp(newValue) == 1 { + newValue = new(big.Int).Set(shrunkenSequenceNewValue) + } + + return shrunkenSequenceNewValue.Cmp(newValue) >= 0, err + }, + FinishedCallback: func(worker *FuzzerWorker, shrunkenCallSequence calls.CallSequence) error { + // When we're finished shrinking, attach an execution trace to the last call + if len(shrunkenCallSequence) > 0 { + err = shrunkenCallSequence[len(shrunkenCallSequence)-1].AttachExecutionTrace(worker.chain, worker.fuzzer.contractDefinitions) + if err != nil { + return err + } + } + + // Execute the property test a final time, this time obtaining an execution trace + shrunkenSequenceNewValue, executionTrace, err := t.runOptimizationTest(worker, &workerOptimizationTestMethod, true) + if err != nil { + return err + } + + // If, for some reason, the shrunken sequence lowers the new max value, do not save anything and exit + if shrunkenSequenceNewValue.Cmp(newValue) < 0 { + return fmt.Errorf("optimized call sequence failed to maximize value") + } + + // Update our value with lock + testCase.valueLock.Lock() + testCase.value = new(big.Int).Set(shrunkenSequenceNewValue) + testCase.valueLock.Unlock() + + // Update call sequence and trace + testCase.callSequence = &shrunkenCallSequence + testCase.optimizationTestTrace = executionTrace + return nil + }, + RecordResultInCorpus: true, + } + + // Add our shrink request to our list. + shrinkRequests = append(shrinkRequests, shrinkRequest) + } + } + + return shrinkRequests, nil +} diff --git a/fuzzing/test_case_property.go b/fuzzing/test_case_property.go index 14b5e7de..9967b1ae 100644 --- a/fuzzing/test_case_property.go +++ b/fuzzing/test_case_property.go @@ -11,10 +11,15 @@ import ( // PropertyTestCase describes a test being run by a PropertyTestCaseProvider. type PropertyTestCase struct { - status TestCaseStatus - targetContract *fuzzerTypes.Contract - targetMethod abi.Method - callSequence *calls.CallSequence + // status describes the status of the test case + status TestCaseStatus + // targetContract describes the target contract where the test case was found + targetContract *fuzzerTypes.Contract + // targetMethod describes the target method for the test case + targetMethod abi.Method + // callSequence describes the call sequence that broke the property + callSequence *calls.CallSequence + // propertyTestTrace describes the execution trace when running the callSequence propertyTestTrace *executiontracer.ExecutionTrace } diff --git a/fuzzing/test_case_property_provider.go b/fuzzing/test_case_property_provider.go index c509807b..ca8be15c 100644 --- a/fuzzing/test_case_property_provider.go +++ b/fuzzing/test_case_property_provider.go @@ -175,7 +175,7 @@ func (t *PropertyTestCaseProvider) onFuzzerStarting(event FuzzerStartingEvent) e return nil } -// onFuzzerStarting is the event handler triggered when the Fuzzer is stopping the fuzzing campaign and all workers +// onFuzzerStopping is the event handler triggered when the Fuzzer is stopping the fuzzing campaign and all workers // have been destroyed. It clears state tracked for each FuzzerWorker and sets test cases in "running" states to // "passed". func (t *PropertyTestCaseProvider) onFuzzerStopping(event FuzzerStoppingEvent) error { @@ -247,7 +247,7 @@ func (t *PropertyTestCaseProvider) onWorkerDeployedContractAdded(event FuzzerWor return nil } -// onWorkerDeployedContractAdded is the event handler triggered when a FuzzerWorker detects that a previously deployed +// onWorkerDeployedContractDeleted is the event handler triggered when a FuzzerWorker detects that a previously deployed // contract no longer exists on its underlying chain. It ensures any property test methods which the deployed contract // contained are no longer tracked by the provider for testing. func (t *PropertyTestCaseProvider) onWorkerDeployedContractDeleted(event FuzzerWorkerContractDeletedEvent) error { diff --git a/fuzzing/testdata/contracts/optimizations/optimize.sol b/fuzzing/testdata/contracts/optimizations/optimize.sol new file mode 100644 index 00000000..3be1e27a --- /dev/null +++ b/fuzzing/testdata/contracts/optimizations/optimize.sol @@ -0,0 +1,14 @@ +contract TestContract { + int256 input; + + function set(int256 _input) public { + input = _input; + } + + function optimize_opt_linear() public view returns (int256) { + if (input > -4242) + return -input; + else + return 0; + } +}