Skip to content

Commit

Permalink
feat: call constant methods occasionally and allow assertion testing …
Browse files Browse the repository at this point in the history
…them (#363)

* feat: call constant methods ocassionally and allow assertion testing them

* add test
  • Loading branch information
0xalpharush authored and s4nsec committed Jul 8, 2024
1 parent 5e5d0e0 commit 827dbed
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 19 deletions.
2 changes: 0 additions & 2 deletions docs/src/project_configuration/testing_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,6 @@ contract MyContract {

- **Type**: Boolean
- **Description**: Whether `pure` / `view` functions should be tested for assertion failures.
> 🚩 Fuzzing `pure` and `view` functions is not currently implemented. Thus, enabling this option to `true` does not
> update the fuzzer's behavior.
- **Default**: `false`

### `panicCodeConfig`
Expand Down
8 changes: 6 additions & 2 deletions fuzzing/fuzzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"github.com/crytic/medusa/fuzzing/executiontracer"
"math/big"
"math/rand"
"os"
Expand All @@ -16,6 +15,8 @@ import (
"sync"
"time"

"github.com/crytic/medusa/fuzzing/executiontracer"

"github.com/crytic/medusa/fuzzing/coverage"
"github.com/crytic/medusa/logging"
"github.com/crytic/medusa/logging/colors"
Expand Down Expand Up @@ -707,7 +708,10 @@ func (f *Fuzzer) Start() error {

// If StopOnNoTests is true and there are no test cases, then throw an error
if f.config.Fuzzing.Testing.StopOnNoTests && len(f.testCases) == 0 {
err = fmt.Errorf("no tests of any kind (assertion/property/optimization/custom) have been identified for fuzzing")
err = fmt.Errorf("no assertion, property, optimization, or custom tests were found to fuzz")
if !f.config.Fuzzing.Testing.AssertionTesting.TestViewMethods {
err = fmt.Errorf("no assertion, property, optimization, or custom tests were found to fuzz and testing view methods is disabled")
}
f.logger.Error("Failed to start fuzzer", err)
return err
}
Expand Down
5 changes: 4 additions & 1 deletion fuzzing/fuzzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ package fuzzing

import (
"encoding/hex"
"github.com/crytic/medusa/fuzzing/executiontracer"
"math/big"
"math/rand"
"testing"

"github.com/crytic/medusa/fuzzing/executiontracer"

"github.com/crytic/medusa/chain"
"github.com/crytic/medusa/events"
"github.com/crytic/medusa/fuzzing/calls"
Expand Down Expand Up @@ -73,6 +74,7 @@ func TestAssertionMode(t *testing.T) {
"testdata/contracts/assertions/assert_outofbounds_array_access.sol",
"testdata/contracts/assertions/assert_allocate_too_much_memory.sol",
"testdata/contracts/assertions/assert_call_uninitialized_variable.sol",
"testdata/contracts/assertions/assert_constant_method.sol",
}
for _, filePath := range filePaths {
runFuzzerTest(t, &fuzzerSolcFileTest{
Expand All @@ -88,6 +90,7 @@ func TestAssertionMode(t *testing.T) {
config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnIncorrectStorageAccess = true
config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnOutOfBoundsArrayAccess = true
config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnPopEmptyArray = true
config.Fuzzing.Testing.AssertionTesting.TestViewMethods = true
config.Fuzzing.Testing.PropertyTesting.Enabled = false
config.Fuzzing.Testing.OptimizationTesting.Enabled = false
},
Expand Down
33 changes: 24 additions & 9 deletions fuzzing/fuzzer_worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/crytic/medusa/fuzzing/valuegeneration"
"github.com/crytic/medusa/logging"
"github.com/crytic/medusa/utils"
"github.com/crytic/medusa/utils/randomutils"
"github.com/ethereum/go-ethereum/common"
"golang.org/x/exp/maps"
)
Expand All @@ -35,11 +36,18 @@ type FuzzerWorker struct {

// deployedContracts describes a mapping of deployed contractDefinitions and the addresses they were deployed to.
deployedContracts map[common.Address]*fuzzerTypes.Contract

// stateChangingMethods is a list of contract functions which are suspected of changing contract state
// (non-read-only). A sequence of calls is generated by the FuzzerWorker, targeting stateChangingMethods
// before executing tests.
stateChangingMethods []fuzzerTypes.DeployedContractMethod

// pureMethods is a list of contract functions which are side-effect free with respect to the EVM (view and/or pure in terms of Solidity mutability).
pureMethods []fuzzerTypes.DeployedContractMethod

// methodChooser uses a weighted selection algorithm to choose a method to call, prioritizing state changing methods over pure ones.
methodChooser *randomutils.WeightedRandomChooser[fuzzerTypes.DeployedContractMethod]

// randomProvider provides random data as inputs to decisions throughout the worker.
randomProvider *rand.Rand
// sequenceGenerator creates entirely new or mutated call sequences based on corpus call sequences, for use in
Expand Down Expand Up @@ -86,6 +94,7 @@ func newFuzzerWorker(fuzzer *Fuzzer, workerIndex int, randomProvider *rand.Rand)
coverageTracer: nil,
randomProvider: randomProvider,
valueSet: valueSet,
methodChooser: randomutils.NewWeightedRandomChooser[fuzzerTypes.DeployedContractMethod](),
}
worker.sequenceGenerator = NewCallSequenceGenerator(worker, callSequenceGenConfig)
worker.shrinkingValueMutator = shrinkingValueMutator
Expand Down Expand Up @@ -175,8 +184,8 @@ func (fw *FuzzerWorker) onChainContractDeploymentAddedEvent(event chain.Contract
// Set our deployed contract address in our deployed contract lookup, so we can reference it later.
fw.deployedContracts[event.Contract.Address] = matchedDefinition

// Update our state changing methods
fw.updateStateChangingMethods()
// Update our methods
fw.updateMethods()

// Emit an event indicating the worker detected a new contract deployment on its chain.
err := fw.Events.ContractAdded.Publish(FuzzerWorkerContractAddedEvent{
Expand Down Expand Up @@ -206,8 +215,8 @@ func (fw *FuzzerWorker) onChainContractDeploymentRemovedEvent(event chain.Contra
// Remove the contract from our deployed contracts mapping the worker maintains.
delete(fw.deployedContracts, event.Contract.Address)

// Update our state changing methods
fw.updateStateChangingMethods()
// Update our methods
fw.updateMethods()

// Emit an event indicating the worker detected the removal of a previously deployed contract on its chain.
err := fw.Events.ContractDeleted.Publish(FuzzerWorkerContractDeletedEvent{
Expand All @@ -221,19 +230,25 @@ func (fw *FuzzerWorker) onChainContractDeploymentRemovedEvent(event chain.Contra
return nil
}

// updateStateChangingMethods updates the list of state changing methods used by the worker by re-evaluating them
// updateMethods updates the list of methods used by the worker by re-evaluating them
// from the deployedContracts lookup.
func (fw *FuzzerWorker) updateStateChangingMethods() {
// Clear our list of state changing methods
func (fw *FuzzerWorker) updateMethods() {
// Clear our list of methods
fw.stateChangingMethods = make([]fuzzerTypes.DeployedContractMethod, 0)
fw.pureMethods = make([]fuzzerTypes.DeployedContractMethod, 0)

// 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.CompiledContract().Abi.Methods {
if !method.IsConstant() {
// Any non-constant method should be tracked as a state changing method.
// Any non-constant method should be tracked as a state changing method.
// 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)))
} else {
fw.stateChangingMethods = append(fw.stateChangingMethods, fuzzerTypes.DeployedContractMethod{Address: contractAddress, Contract: contractDefinition, Method: method})
fw.methodChooser.AddChoices(randomutils.NewWeightedRandomChoice(fuzzerTypes.DeployedContractMethod{Address: contractAddress, Contract: contractDefinition, Method: method}, big.NewInt(100)))
}
}
}
Expand Down
13 changes: 8 additions & 5 deletions fuzzing/fuzzer_worker_sequence_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,17 +270,20 @@ func (g *CallSequenceGenerator) PopSequenceElement() (*calls.CallSequenceElement
return element, nil
}

// generateNewElement generates a new call sequence element which targets a state changing method in a contract
// generateNewElement generates a new call sequence element which targets a method in a contract
// deployed to the CallSequenceGenerator's parent FuzzerWorker chain, with fuzzed call data.
// Returns the call sequence element, or an error if one was encountered.
func (g *CallSequenceGenerator) generateNewElement() (*calls.CallSequenceElement, error) {
// Verify we have state changing methods to call
if len(g.worker.stateChangingMethods) == 0 {
// Verify we have state changing methods to call if we are not testing view/pure methods.
if len(g.worker.stateChangingMethods) == 0 && !g.worker.fuzzer.config.Fuzzing.Testing.AssertionTesting.TestViewMethods {
return nil, fmt.Errorf("cannot generate fuzzed tx as there are no state changing methods to call")
}

// Select a random method and sender
selectedMethod := &g.worker.stateChangingMethods[g.worker.randomProvider.Intn(len(g.worker.stateChangingMethods))]
selectedMethod, err := g.worker.methodChooser.Choose()
if err != nil {
return nil, err
}

selectedSender := g.worker.fuzzer.senders[g.worker.randomProvider.Intn(len(g.worker.fuzzer.senders))]

// Generate fuzzed parameters for the function call
Expand Down
148 changes: 148 additions & 0 deletions fuzzing/testdata/contracts/assertions/assert_constant_method.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// An assertion failure in the constant method should be detected
// Contract updated to solc > 0.8.0 and taken from https://github.com/crytic/echidna/blob/5757f8c3c07d0248cbe1728506ff0f8daccbef12/tests/solidity/assert/fullmath.sol
library FullMath {
/// @notice Calculates floor(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0
/// @param a The multiplicand
/// @param b The multiplier
/// @param denominator The divisor
/// @return result The 256-bit result
/// @dev Credit to Remco Bloemen under MIT license https://xn--2-umb.com/21/muldiv
function mulDiv(
uint256 a,
uint256 b,
uint256 denominator
) internal pure returns (uint256 result) {
unchecked {


// 512-bit multiply [prod1 prod0] = a * b
// Compute the product mod 2**256 and mod 2**256 - 1
// then use the Chinese Remainder Theorem to reconstruct
// the 512 bit result. The result is stored in two 256
// variables such that product = prod1 * 2**256 + prod0
uint256 prod0; // Least significant 256 bits of the product
uint256 prod1; // Most significant 256 bits of the product
assembly {
let mm := mulmod(a, b, not(0))
prod0 := mul(a, b)
prod1 := sub(sub(mm, prod0), lt(mm, prod0))
}

// Handle non-overflow cases, 256 by 256 division
if (prod1 == 0) {
require(denominator > 0);
assembly {
result := div(prod0, denominator)
}
return result;
}

// Make sure the result is less than 2**256.
// Also prevents denominator == 0
require(denominator > prod1);

///////////////////////////////////////////////
// 512 by 256 division.
///////////////////////////////////////////////

// Make division exact by subtracting the remainder from [prod1 prod0]
// Compute remainder using mulmod
uint256 remainder;
assembly {
remainder := mulmod(a, b, denominator)
}
// Subtract 256 bit number from 512 bit number
assembly {
prod1 := sub(prod1, gt(remainder, prod0))
prod0 := sub(prod0, remainder)
}

// Factor powers of two out of denominator
// Compute largest power of two divisor of denominator.
// Always >= 1.
uint256 twos = (0 - denominator) & denominator;
// Divide denominator by power of two
assembly {
denominator := div(denominator, twos)
}

// Divide [prod1 prod0] by the factors of two
assembly {
prod0 := div(prod0, twos)
}
// Shift in bits from prod1 into prod0. For this we need
// to flip `twos` such that it is 2**256 / twos.
// If twos is zero, then it becomes one
assembly {
twos := add(div(sub(0, twos), twos), 1)
}
prod0 |= prod1 * twos;

// Invert denominator mod 2**256
// Now that denominator is an odd number, it has an inverse
// modulo 2**256 such that denominator * inv = 1 mod 2**256.
// Compute the inverse by starting with a seed that is correct
// correct for four bits. That is, denominator * inv = 1 mod 2**4
uint256 inv = (3 * denominator) ^ 2;
// Now use Newton-Raphson iteration to improve the precision.
// Thanks to Hensel's lifting lemma, this also works in modular
// arithmetic, doubling the correct bits in each step.
inv *= 2 - denominator * inv; // inverse mod 2**8
inv *= 2 - denominator * inv; // inverse mod 2**16
inv *= 2 - denominator * inv; // inverse mod 2**32
inv *= 2 - denominator * inv; // inverse mod 2**64
inv *= 2 - denominator * inv; // inverse mod 2**128
inv *= 2 - denominator * inv; // inverse mod 2**256

// Because the division is now exact we can divide by multiplying
// with the modular inverse of denominator. This will give us the
// correct result modulo 2**256. Since the precoditions guarantee
// that the outcome is less than 2**256, this is the final result.
// We don't need to compute the high bits of the result and prod1
// is no longer required.
result = prod0 * inv;
return result;
}
}

/// @notice Calculates ceil(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0
/// @param a The multiplicand
/// @param b The multiplier
/// @param denominator The divisor
/// @return result The 256-bit result
function mulDivRoundingUp(
uint256 a,
uint256 b,
uint256 denominator
) internal pure returns (uint256 result) {
// BUG
unchecked {
return mulDiv(a, b, denominator) + (mulmod(a, b, denominator) > 0 ? 1 : 0);
}
}
}

contract TestContract {
function checkMulDivRoundingUp(
uint256 x,
uint256 y,
uint256 d
) external pure {
require(d > 0);
uint256 z = FullMath.mulDivRoundingUp(x, y, d);
if (x == 0 || y == 0) {
assert(z == 0);
return;
}

// recompute x and y via mulDiv of the result of floor(x*y/d), should always be less than original inputs by < d
uint256 x2 = FullMath.mulDiv(z, d, y);
uint256 y2 = FullMath.mulDiv(z, d, x);
assert(x2 >= x);
assert(y2 >= y);

assert(x2 - x < d);
assert(y2 - y < d);
}
fallback() external {}
}

0 comments on commit 827dbed

Please sign in to comment.