Skip to content

Commit

Permalink
Added denomination parsing to AST value extraction (#202)
Browse files Browse the repository at this point in the history
* Added denomination parsing to AST constant mining
* Added tests for AST value extraction, removed print from experimental code

---------

Co-authored-by: anishnaik <[email protected]>
  • Loading branch information
Xenomega and anishnaik authored Aug 18, 2023
1 parent 7d2e16c commit 45e26de
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 7 deletions.
24 changes: 18 additions & 6 deletions fuzzing/fuzzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"math/big"
"math/rand"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -723,13 +724,24 @@ func (f *Fuzzer) printMetricsLoop() {
// Calculate time elapsed since the last update
secondsSinceLastUpdate := time.Since(lastPrintedTime).Seconds()

// Obtain memory usage stats
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
memoryUsedMB := memStats.Alloc / 1024 / 1024
memoryTotalMB := memStats.Sys / 1024 / 1024

// Print a metrics update
f.logger.Info(colors.Bold, "fuzz: ", colors.Reset,
"elapsed: ", colors.Bold, time.Since(startTime).Round(time.Second).String(), colors.Reset,
", calls: ", colors.Bold, fmt.Sprintf("%d (%d/sec)", callsTested, uint64(float64(new(big.Int).Sub(callsTested, lastCallsTested).Uint64())/secondsSinceLastUpdate)), colors.Reset,
", seq/s: ", colors.Bold, fmt.Sprintf("%d", uint64(float64(new(big.Int).Sub(sequencesTested, lastSequencesTested).Uint64())/secondsSinceLastUpdate)), colors.Reset,
", resets/s: ", colors.Bold, fmt.Sprintf("%d", uint64(float64(new(big.Int).Sub(workerStartupCount, lastWorkerStartupCount).Uint64())/secondsSinceLastUpdate)), colors.Reset,
", coverage: ", colors.Bold, fmt.Sprintf("%d", f.corpus.ActiveMutableSequenceCount()), colors.Reset)
logBuffer := logging.NewLogBuffer()
logBuffer.Append(colors.Bold, "fuzz: ", colors.Reset)
logBuffer.Append("elapsed: ", colors.Bold, time.Since(startTime).Round(time.Second).String(), colors.Reset)
logBuffer.Append(", calls: ", colors.Bold, fmt.Sprintf("%d (%d/sec)", callsTested, uint64(float64(new(big.Int).Sub(callsTested, lastCallsTested).Uint64())/secondsSinceLastUpdate)), colors.Reset)
logBuffer.Append(", seq/s: ", colors.Bold, fmt.Sprintf("%d", uint64(float64(new(big.Int).Sub(sequencesTested, lastSequencesTested).Uint64())/secondsSinceLastUpdate)), colors.Reset)
logBuffer.Append(", coverage: ", colors.Bold, fmt.Sprintf("%d", f.corpus.ActiveMutableSequenceCount()), colors.Reset)
if f.logger.Level() <= zerolog.DebugLevel {
logBuffer.Append(", mem: ", colors.Bold, fmt.Sprintf("%v/%v MB", memoryUsedMB, memoryTotalMB), colors.Reset)
logBuffer.Append(", resets/s: ", colors.Bold, fmt.Sprintf("%d", uint64(float64(new(big.Int).Sub(workerStartupCount, lastWorkerStartupCount).Uint64())/secondsSinceLastUpdate)), colors.Reset)
}
f.logger.Info(logBuffer.Elements()...)

// Update our delta tracking metrics
lastPrintedTime = time.Now()
Expand Down
71 changes: 71 additions & 0 deletions fuzzing/fuzzer_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package fuzzing

import (
"encoding/hex"
"github.com/crytic/medusa/chain"
"github.com/crytic/medusa/events"
"github.com/crytic/medusa/fuzzing/calls"
"github.com/crytic/medusa/fuzzing/valuegeneration"
"github.com/crytic/medusa/utils"
"github.com/ethereum/go-ethereum/common"
"math/big"
"math/rand"
"testing"
Expand Down Expand Up @@ -595,6 +597,75 @@ func TestValueGenerationSolving(t *testing.T) {
}
}

// TestASTValueExtraction runs a test to ensure appropriate AST values can be mined out of a compiled source's AST.
func TestASTValueExtraction(t *testing.T) {
// Define our expected values to be mined.
expectedAddresses := []common.Address{
common.HexToAddress("0x7109709ECfa91a80626fF3989D68f67F5b1DD12D"),
common.HexToAddress("0x1234567890123456789012345678901234567890"),
}
expectedIntegers := []string{
// Unsigned integer tests
"111", // no denomination
"1", // 1 wei (base unit)
"2000000000", // 2 gwei
"5000000000000000000", // 5 ether
"6", // 6 seconds (base unit)
"420", // 7 minutes
"28800", // 8 hours
"777600", // 9 days
"6048000", // 10 weeks

// Signed integer tests
"-111", // no denomination
"-1", // 1 wei (base unit)
"-2000000000", // 2 gwei
"-5000000000000000000", // 5 ether
"-6", // 6 seconds (base unit)
"-420", // 7 minutes
"-28800", // 8 hours
"-777600", // 9 days
"-6048000", // 10 weeks
}
expectedStrings := []string{
"testString",
"testString2",
}
expectedByteSequences := make([][]byte, 0) // no tests yet

// Run the fuzzer test
runFuzzerTest(t, &fuzzerSolcFileTest{
filePath: "testdata/contracts/value_generation/ast_value_extraction.sol",
configUpdates: func(config *config.ProjectConfig) {
config.Fuzzing.TestLimit = 1 // stop immediately to simply see what values were mined.
config.Fuzzing.Testing.AssertionTesting.Enabled = true
config.Fuzzing.Testing.PropertyTesting.Enabled = false
},
method: func(f *fuzzerTestContext) {
// Start the fuzzer
err := f.fuzzer.Start()
assert.NoError(t, err)

// Verify all of our expected values exist
valueSet := f.fuzzer.BaseValueSet()
for _, expectedAddr := range expectedAddresses {
assert.True(t, valueSet.ContainsAddress(expectedAddr), "Value set did not contain expected address: %v", expectedAddr.String())
}
for _, expectedIntegerStr := range expectedIntegers {
expectedInteger, ok := new(big.Int).SetString(expectedIntegerStr, 10)
assert.True(t, ok, "Could not parse provided expected integer string in test: \"%v\"", expectedIntegerStr)
assert.True(t, valueSet.ContainsInteger(expectedInteger), "Value set did not contain expected integer: %v", expectedInteger.String())
}
for _, expectedString := range expectedStrings {
assert.True(t, valueSet.ContainsString(expectedString), "Value set did not contain expected string: \"%v\"", expectedString)
}
for _, expectedByteSequence := range expectedByteSequences {
assert.True(t, valueSet.ContainsBytes(expectedByteSequence), "Value set did not contain expected bytes: \"%v\"", hex.EncodeToString(expectedByteSequence))
}
},
})
}

// TestVMCorrectness runs tests to ensure block properties are reported consistently within the EVM, as it's configured
// by the chain.TestChain.
func TestVMCorrectness(t *testing.T) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// This contract verifies the fuzzer can extract AST literals of different subdenominations from the file.
contract TestContract {
function addressValues() public {
address x = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D;
assert(x != address(0x1234567890123456789012345678901234567890));
}
function uintValues() public {
// Use all integer denoms
uint x = 111;
x = 1 wei;
x = 2 gwei;
//x = 3 szabo;
//x = 4 finney;
x = 5 ether;
x = 6 seconds;
x = 7 minutes;
x = 8 hours;
x = 9 days;
x = 10 weeks;
//x = 11 years;

// Dummy assertion that should always pass.
assert(x != 0);
}
function intValues() public {
// Use all integer denoms
int x = -111;
x = -1 wei;
x = -2 gwei;
//x = -3 szabo;
//x = -4 finney;
x = -5 ether;
x = -6 seconds;
x = -7 minutes;
x = -8 hours;
x = -9 days;
x = -10 weeks;
//x = -11 years;

// Dummy assertion that should always pass.
assert(x != 0);
}
function stringValues() public {
string memory s = "testString";
s = "testString2";
assert(true);
}
}
30 changes: 30 additions & 0 deletions fuzzing/valuegeneration/value_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ func (vs *ValueSet) AddAddress(a common.Address) {
vs.addresses[a] = nil
}

// ContainsAddress checks if an address is contained in the ValueSet.
func (vs *ValueSet) ContainsAddress(a common.Address) bool {
_, contains := vs.addresses[a]
return contains
}

// RemoveAddress removes an address item from the ValueSet.
func (vs *ValueSet) RemoveAddress(a common.Address) {
delete(vs.addresses, a)
Expand All @@ -85,6 +91,12 @@ func (vs *ValueSet) AddInteger(b *big.Int) {
vs.integers[b.String()] = b
}

// ContainsInteger checks if an integer is contained in the ValueSet.
func (vs *ValueSet) ContainsInteger(b *big.Int) bool {
_, contains := vs.integers[b.String()]
return contains
}

// RemoveInteger removes an integer item from the ValueSet.
func (vs *ValueSet) RemoveInteger(b *big.Int) {
delete(vs.integers, b.String())
Expand All @@ -106,6 +118,12 @@ func (vs *ValueSet) AddString(s string) {
vs.strings[s] = nil
}

// ContainsString checks if a string is contained in the ValueSet.
func (vs *ValueSet) ContainsString(s string) bool {
_, contains := vs.strings[s]
return contains
}

// RemoveString removes a string item from the ValueSet.
func (vs *ValueSet) RemoveString(s string) {
delete(vs.strings, s)
Expand Down Expand Up @@ -133,6 +151,18 @@ func (vs *ValueSet) AddBytes(b []byte) {
vs.bytes[hashStr] = b
}

// ContainsBytes checks if a byte sequence is contained in the ValueSet.
func (vs *ValueSet) ContainsBytes(b []byte) bool {
// Calculate hash and reset our hash provider
vs.hashProvider.Write(b)
hashStr := hex.EncodeToString(vs.hashProvider.Sum(nil))
vs.hashProvider.Reset()

// Check if the key exists in our lookup
_, contains := vs.bytes[hashStr]
return contains
}

// RemoveBytes removes a byte sequence item from the ValueSet.
func (vs *ValueSet) RemoveBytes(b []byte) {
// Calculate hash and reset our hash provider
Expand Down
56 changes: 55 additions & 1 deletion fuzzing/valuegeneration/value_set_from_ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package valuegeneration

import (
"github.com/ethereum/go-ethereum/common"
"github.com/shopspring/decimal"
"math/big"
"strings"
)
Expand All @@ -20,16 +21,25 @@ func (vs *ValueSet) SeedFromAst(ast any) {
return // fail silently to continue walking
}

// Extract the subdenomination type
tempSubdenomination, obtainedSubdenomination := node["subdenomination"].(string)
var literalSubdenomination *string
if obtainedSubdenomination {
literalSubdenomination = &tempSubdenomination
}

// Seed ValueSet with literals
if literalKind == "number" {
// If it has a 0x prefix, it won't have decimals
if strings.HasPrefix(literalValue, "0x") {
if b, ok := big.NewInt(0).SetString(literalValue[2:], 16); ok {
vs.AddInteger(b)
vs.AddInteger(new(big.Int).Neg(b))
vs.AddAddress(common.BigToAddress(b))
}
} else {
if b, ok := big.NewInt(0).SetString(literalValue, 10); ok {
if decValue, err := decimal.NewFromString(literalValue); err == nil {
b := getAbsoluteValueFromDenominatedValue(decValue, literalSubdenomination)
vs.AddInteger(b)
vs.AddInteger(new(big.Int).Neg(b))
vs.AddAddress(common.BigToAddress(b))
Expand All @@ -42,6 +52,50 @@ func (vs *ValueSet) SeedFromAst(ast any) {
})
}

// getAbsoluteValueFromDenominatedValue converts a given decimal number in a provided denomination to a big.Int
// that represents its actual calculated value.
// Note: Decimals must be used as big.Float is prone to similar mantissa-related precision issues as float32/float64.
// Returns the calculated value given the floating point number in a given denomination.
func getAbsoluteValueFromDenominatedValue(number decimal.Decimal, denomination *string) *big.Int {
// If the denomination is nil, we do nothing
if denomination == nil {
return number.BigInt()
}

// Otherwise, switch on the type and obtain a multiplier
var multiplier decimal.Decimal
switch *denomination {
case "wei":
multiplier = decimal.NewFromFloat32(1)
case "gwei":
multiplier = decimal.NewFromFloat32(1e9)
case "szabo":
multiplier = decimal.NewFromFloat32(1e12)
case "finney":
multiplier = decimal.NewFromFloat32(1e15)
case "ether":
multiplier = decimal.NewFromFloat32(1e18)
case "seconds":
multiplier = decimal.NewFromFloat32(1)
case "minutes":
multiplier = decimal.NewFromFloat32(60)
case "hours":
multiplier = decimal.NewFromFloat32(60 * 60)
case "days":
multiplier = decimal.NewFromFloat32(60 * 60 * 24)
case "weeks":
multiplier = decimal.NewFromFloat32(60 * 60 * 24 * 7)
case "years":
multiplier = decimal.NewFromFloat32(60 * 60 * 24 * 7 * 365)
default:
multiplier = decimal.NewFromFloat32(1)
}

// Obtain the transformed number as an integer.
transformedValue := number.Mul(multiplier)
return transformedValue.BigInt()
}

// walkAstNodes walks/iterates across an AST for each node, calling the provided walk function with each discovered node
// as an argument.
func walkAstNodes(ast any, walkFunc func(node map[string]any)) {
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/google/uuid v1.3.0
github.com/pkg/errors v0.9.1
github.com/rs/zerolog v1.29.0
github.com/shopspring/decimal v1.3.1
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.4
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@ github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtm
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
Expand Down

0 comments on commit 45e26de

Please sign in to comment.