Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for optimization mode #75

Merged
merged 12 commits into from
Jun 21, 2023
Merged
12 changes: 12 additions & 0 deletions fuzzing/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions fuzzing/config/config_defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) {
"fuzz_",
},
},
OptimizationTesting: OptimizationTestingConfig{
Enabled: false,
TestPrefixes: []string{
"optimize_",
},
},
},
TestChainConfig: *chainConfig,
},
Expand Down
5 changes: 5 additions & 0 deletions fuzzing/fuzzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
34 changes: 34 additions & 0 deletions fuzzing/fuzzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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.
Expand Down
10 changes: 7 additions & 3 deletions fuzzing/test_case_assertion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion fuzzing/test_case_assertion_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
77 changes: 77 additions & 0 deletions fuzzing/test_case_optimization.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading