diff --git a/lib/Echidna/Utility.hs b/lib/Echidna/Utility.hs index c73d855a8..8fd46701c 100644 --- a/lib/Echidna/Utility.hs +++ b/lib/Echidna/Utility.hs @@ -4,6 +4,7 @@ import Control.Monad (unless) import Control.Monad.Catch (bracket) import Data.Time (diffUTCTime, getCurrentTime, zonedTimeToLocalTime, LocalTime, getZonedTime) import Data.Time.Format (defaultTimeLocale, formatTime) +import Language.Haskell.TH import System.Directory (getDirectoryContents, getCurrentDirectory, setCurrentDirectory) import System.IO (hFlush, stdout) @@ -38,3 +39,6 @@ withCurrentDirectory dir action = bracket getCurrentDirectory setCurrentDirectory $ \_ -> do setCurrentDirectory dir action + +includeFile :: FilePath -> Q Exp +includeFile fp = LitE . StringL <$> runIO (readFile fp) diff --git a/package.yaml b/package.yaml index e8cf3454e..4564204ae 100644 --- a/package.yaml +++ b/package.yaml @@ -49,6 +49,7 @@ dependencies: - http-conduit - html-conduit - xml-conduit + - template-haskell language: GHC2021 diff --git a/src/Main.hs b/src/Main.hs index 9eef594c8..c4ecb2e7f 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -1,4 +1,5 @@ {-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TemplateHaskell #-} module Main where @@ -53,12 +54,27 @@ import Echidna.Output.Source import Echidna.Output.Corpus import Echidna.RPC qualified as RPC import Echidna.Solidity (compileContracts, selectBuildOutput) -import Echidna.Utility (measureIO) +import Echidna.Utility (includeFile, measureIO) import Etherscan qualified main :: IO () main = withUtf8 $ withCP65001 $ do - opts@Options{..} <- execParser optsParser + cli <- execParser cliParser + case cli of + InitCommand -> do + let config = "echidna.yaml" + configExists <- doesFileExist config + if configExists then do + putStrLn $ "Config file " <> config <> " already exists." + exitWith (ExitFailure 1) + else do + writeFile config $(includeFile "tests/solidity/basic/default.yaml") + putStrLn $ "Sample config file written to " <> config + FuzzCommand fuzzOpts -> + fuzz fuzzOpts + +fuzz :: FuzzOptions -> IO () +fuzz opts@FuzzOptions{..} = do EConfigWithUsage loadedCfg ks _ <- maybe (pure (EConfigWithUsage defaultConfig mempty mempty)) parseConfig cliConfigFilepath cfg <- overrideConfig loadedCfg opts @@ -222,7 +238,11 @@ readFileIfExists path = do exists <- doesFileExist path if exists then Just <$> BS.readFile path else pure Nothing -data Options = Options +data CLI + = InitCommand + | FuzzCommand FuzzOptions + +data FuzzOptions = FuzzOptions { cliFilePath :: NE.NonEmpty FilePath , cliWorkers :: Maybe Word8 , cliSelectedContract :: Maybe Text @@ -243,13 +263,19 @@ data Options = Options , cliSolcArgs :: Maybe String } -optsParser :: ParserInfo Options -optsParser = info (helper <*> versionOption <*> options) $ fullDesc +cliParser :: ParserInfo CLI +cliParser = info (helper <*> versionOption <*> commands) $ fullDesc <> progDesc "EVM property-based testing framework" <> header "Echidna" - -options :: Parser Options -options = Options + where + commands = subparser $ + command "init" (info (pure InitCommand) + (progDesc "Write a sample config file to echidna.yaml")) + <> command "fuzz" (info (FuzzCommand <$> fuzzOptions) + (progDesc "Run fuzzing")) + +fuzzOptions :: Parser FuzzOptions +fuzzOptions = FuzzOptions <$> (NE.fromList <$> some (argument str (metavar "FILES" <> help "Solidity files to analyze"))) <*> optional (option auto $ long "workers" @@ -307,8 +333,8 @@ versionOption = infoOption ("Echidna " ++ showVersion version) (long "version" <> help "Show version") -overrideConfig :: EConfig -> Options -> IO EConfig -overrideConfig config Options{..} = do +overrideConfig :: EConfig -> FuzzOptions -> IO EConfig +overrideConfig config FuzzOptions{..} = do rpcUrl <- RPC.rpcUrlEnv rpcBlock <- RPC.rpcBlockEnv pure $ @@ -350,4 +376,3 @@ overrideConfig config Options{..} = do , testMode = maybe solConf.testMode validateTestMode cliTestMode , allContracts = cliAllContracts || solConf.allContracts } - diff --git a/tests/solidity/basic/default.yaml b/tests/solidity/basic/default.yaml index 9b2c6f71a..154dda3ba 100644 --- a/tests/solidity/basic/default.yaml +++ b/tests/solidity/basic/default.yaml @@ -1,91 +1,116 @@ -# TODO -#select the mode to test, which can be property, assertion, overflow, exploration, optimization +# Number of fuzzing workers to run, should not exceed the number of available cores. +workers: 1 + +# Test mode, one of: property, assertion, overflow, exploration, optimization. testMode: "property" -#check if some contract was destructed or not + +# Directory to save the corpus and coverage reports; disabled by default +corpusDir: null +# List of file formats to save coverage reports in; default is all possible formats +coverageFormats: ["txt","html","lcov"] +# If specified, disables the interactive UI and prints the results to stdout. +# Can be "text", "json" or "none". +format: null +# Produces (much) less verbose output +quiet: false + +# Check if some contract was destructed or not testDestruction: false -#psender is the sender for property transactions; by default intentionally -#the same as contract deployer + +# psender is the sender for property transactions; by default intentionally +# the same as contract deployer psender: "0x10000" -#prefix is the prefix for Boolean functions that are properties to be checked + +# Prefix used to find property functions. Property functions don't take any +# arguments and return bool. prefix: "echidna_" -#propMaxGas defines gas cost at which a property fails -propMaxGas: 8000030 -#testMaxGas is a gas limit; does not cause failure, but terminates sequence -testMaxGas: 8000030 -#maxGasprice is the maximum gas price -maxGasprice: 0 -#testLimit is the number of test sequences to run -testLimit: 50000 -#stopOnFail makes echidna terminate as soon as any property fails and has been shrunk -stopOnFail: false -#estimateGas makes echidna perform analysis of maximum gas costs for functions (experimental) -estimateGas: false -#seqLen defines how many transactions are in a test sequence + +# The number of transactions generated in a test sequence. seqLen: 100 -#shrinkLimit determines how much effort is spent shrinking failing sequences +# The number of test sequences to run. +testLimit: 50000 +# How many attemts to run when shrinking the failing sequences. shrinkLimit: 5000 -#coverage controls coverage guided testing -coverage: false -#format can be "text" or "json" for different output (human or machine readable) -format: "text" -#contractAddr is the address of the contract itself + +# Stop fuzzing as soon as any property fails and has been shrunk. +stopOnFail: false +# Whether coverage-guided fuzzing is enabled. +coverage: true +# Address of the contract itself contractAddr: "0x00a329c0648769a73afac7f9381e08fb43dbea72" -#deployer is address of the contract deployer (who often is privileged owner, etc.) +# Address of the contract deployer (who often is privileged owner, etc.) deployer: "0x30000" -#sender is set of addresses transactions may originate from +# Set of addresses transactions may originate from sender: ["0x10000", "0x20000", "0x30000"] -#balanceAddr is default balance for addresses +# Default balance for addresses balanceAddr: 0xffffffff -#balanceContract overrides balanceAddr for the contract address +# Overrides balanceAddr for the contract address balanceContract: 0 -#codeSize max code size for deployed contratcs (default 24576, per EIP-170) +# Max code size for deployed contratcs (default 24576, per EIP-170) codeSize: 0x6000 -#solcArgs allows special args to solc + +# Pass additional CLI options to crytic-compile. +# See: https://github.com/crytic/crytic-compile +cryticArgs: [] +# Pass additional CLI options to solc. solcArgs: "" -#solcLibs is solc libraries +# solcLibs is solc libraries solcLibs: [] -#cryticArgs allows special args to crytic -cryticArgs: [] -#quiet produces (much) less verbose output -quiet: false -#initialize the blockchain with some data + +# Initialize the blockchain with some data initialize: null -#initialize the blockchain with some predeployed contracts in some addresses +# Initialize the blockchain with some predeployed contracts in some addresses deployContracts: [] -#initialize the blockchain with some bytecode in some addresses +# Initialize the blockchain with some bytecode in some addresses deployBytecodes: [] -#whether ot not to fuzz all contracts +# Whether ot not to fuzz all contracts allContracts: false -#timeout controls test timeout settings + +# Set a timeout to stop fuzzing after N seconds. timeout: null -#seed not defined by default, is the random seed -#seed: 0 -#dictFreq controls how often to use echidna's internal dictionary vs random -#values + +# Use to fix the seed for random number generator. If not specified, a new +# random seed value is used every time. A positive integer. +# seed: 0 + +# How often to use echidna's internal dictionary vs random values while fuzzing. +# Value between 0 and 1. dictFreq: 0.40 + +# Defines gas cost at which a property fails +propMaxGas: 8000030 +# Gas limit; does not cause failure, but terminates a sequence +testMaxGas: 8000030 +# Maximum gas price +maxGasprice: 0 +# Maximum value to send to payable functions +maxValue: 100000000000000000000 # 100 eth +# Maximum time between generated txs; default is one week maxTimeDelay: 604800 -#maximum time between generated txs; default is one week +# Maximum number of blocks elapsed between generated txs; default is expected +# increment in one week maxBlockDelay: 60480 -#maximum number of blocks elapsed between generated txs; default is expected increment in one week -# timeout: -#campaign timeout (in seconds) -# list of methods to filter + +# List of methods to filter filterFunctions: [] # by default, blacklist methods in filterFunctions filterBlacklist: true -# enable or disable ffi HEVM cheatcode + +# Enable the ffi HEVM cheatcode. It is disabled by default for security. +# See: https://hevm.dev/controlling-the-unit-testing-environment.html. allowFFI: false -#directory to save the corpus; by default is disabled -corpusDir: null -# list of file formats to save coverage reports in; default is all possible formats -coverageFormats: ["txt","html","lcov"] -# constants for corpus mutations (for experimentation only) -mutConsts: [1, 1, 1, 1] -# maximum value to send to payable functions -maxValue: 100000000000000000000 # 100 eth + +# Configure to perform "on-chain fuzzing". +# See: https://blog.trailofbits.com/2023/07/21/fuzzing-on-chain-contracts-with-echidna/ # URL to fetch contracts over RPC rpcUrl: null -# block number to use when fetching over RPC +# Block number to use when fetching over RPC rpcBlock: null -# number of workers -workers: 1 + +# === +# NOTE: The experimental options below shouldn't be touched unless you know what you are doing. +# === +# Constants for corpus mutations (for experimentation only) +mutConsts: [1, 1, 1, 1] +# Perform analysis of maximum gas costs for functions (experimental) +estimateGas: false