From 9c8e303592a1a126b6e8c4d738cfda1c10c55b4d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 May 2024 14:34:38 +0200 Subject: [PATCH 01/29] --- (#1263) updated-dependencies: - dependency-name: cachix/install-nix-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/hlint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hlint.yml b/.github/workflows/hlint.yml index f5b54a33d..f81257ec6 100644 --- a/.github/workflows/hlint.yml +++ b/.github/workflows/hlint.yml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v4 - name: Install Nix - uses: cachix/install-nix-action@v26 + uses: cachix/install-nix-action@V27 with: nix_path: nixpkgs=channel:nixos-unstable From acaaaede315de86bf0102223d1ce0cd7d0f44978 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 May 2024 14:34:54 +0200 Subject: [PATCH 02/29] --- (#1262) updated-dependencies: - dependency-name: cachix/cachix-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 251771108..6c17c555d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,7 +39,7 @@ jobs: uses: DeterminateSystems/nix-installer-action@v11 - name: Configure Cachix - uses: cachix/cachix-action@v14 + uses: cachix/cachix-action@v15 with: name: trailofbits authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} From 53a0a912a1e6360ec780d98f069e2f43a650f0f9 Mon Sep 17 00:00:00 2001 From: Gustavo Grieco <31542053+ggrieco-tob@users.noreply.github.com> Date: Tue, 28 May 2024 15:54:55 +0200 Subject: [PATCH 03/29] avoid a crash when invalid filtering is used and provide a better error message (#1258) --- lib/Echidna/Solidity.hs | 3 +++ lib/Echidna/Types/Solidity.hs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/Echidna/Solidity.hs b/lib/Echidna/Solidity.hs index b75c19f55..1868ff246 100644 --- a/lib/Echidna/Solidity.hs +++ b/lib/Echidna/Solidity.hs @@ -206,6 +206,9 @@ loadSpecified env name cs = do Just ne -> Map.singleton mainContract.runtimeCodehash ne Nothing -> mempty + when (Map.null abiMapping) $ + throwM $ InvalidMethodFilters solConf.methodFilter + -- Set up initial VM, either with chosen contract or Etheno initialization file -- need to use snd to add to ABI dict initVM <- stToIO $ initialVM solConf.allowFFI diff --git a/lib/Echidna/Types/Solidity.hs b/lib/Echidna/Types/Solidity.hs index 82bcc7a59..792c1b269 100644 --- a/lib/Echidna/Types/Solidity.hs +++ b/lib/Echidna/Types/Solidity.hs @@ -49,7 +49,7 @@ instance Show SolException where OnlyTests -> "Only tests and no public functions found in ABI" ConstructorArgs s -> "Constructor arguments are required: " ++ s NoCryticCompile -> "crytic-compile not installed or not found in PATH. To install it, run:\n pip install crytic-compile" - InvalidMethodFilters f -> "Applying " ++ show f ++ " to the methods produces an empty list. Are you filtering the correct functions or fuzzing the correct contract?" + InvalidMethodFilters f -> "Applying the filter " ++ show f ++ " to the methods produces an empty list. Are you filtering the correct functions using `filterFunctions` or fuzzing the correct contract?" SetUpCallFailed -> "Calling the setUp() function failed (revert, out-of-gas, sending ether to an non-payable constructor, etc.)" DeploymentFailed a t -> "Deploying the contract " ++ show a ++ " failed (revert, out-of-gas, sending ether to an non-payable constructor, etc.):\n" ++ unpack t OutdatedSolcVersion v -> "Solc version " ++ toString v ++ " detected. Echidna doesn't support versions of solc before " ++ toString minSupportedSolcVersion ++ ". Please use a newer version." From 80acdf5d748a0d15e22bf4010e915ddc64278a0d Mon Sep 17 00:00:00 2001 From: Gustavo Grieco <31542053+ggrieco-tob@users.noreply.github.com> Date: Tue, 28 May 2024 19:28:48 +0200 Subject: [PATCH 04/29] Allow to use specific filter for direct symexec (#1251) * Allow to use specific filter for direct symexec * Update default.yaml --- lib/Echidna/Campaign.hs | 2 +- lib/Echidna/Config.hs | 1 + lib/Echidna/SymExec.hs | 6 +++++- lib/Echidna/Types/Campaign.hs | 1 + src/Main.hs | 5 +++++ tests/solidity/basic/default.yaml | 3 +++ 6 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/Echidna/Campaign.hs b/lib/Echidna/Campaign.hs index 7e95b1f83..1b07cfa1e 100644 --- a/lib/Echidna/Campaign.hs +++ b/lib/Echidna/Campaign.hs @@ -185,7 +185,7 @@ runSymWorker callback vm dict workerId initialCorpus name cs = do -- We can't do callseq vm' [symTx] because callseq might post the full call sequence as an event newCoverage <- or <$> mapM (\symTx -> snd <$> callseq vm (txsBase <> [symTx])) symTxs - unless newCoverage (pushWorkerEvent SymNoNewCoverage) + unless (newCoverage || null symTxs) (pushWorkerEvent SymNoNewCoverage) -- | Run a fuzzing campaign given an initial universe state, some tests, and an -- optional dictionary to generate calls with. Return the 'Campaign' state once diff --git a/lib/Echidna/Config.hs b/lib/Echidna/Config.hs index ac332f17d..cbe6ca17e 100644 --- a/lib/Echidna/Config.hs +++ b/lib/Echidna/Config.hs @@ -100,6 +100,7 @@ instance FromJSON EConfigWithUsage where <*> v ..:? "server" <*> v ..:? "symExec" ..!= False <*> v ..:? "symExecConcolic" ..!= True + <*> v ..:? "symExecTargets" ..!= Nothing <*> v ..:? "symExecTimeout" ..!= defaultSymExecTimeout <*> v ..:? "symExecNSolvers" ..!= defaultSymExecNWorkers <*> v ..:? "symExecMaxIters" ..!= defaultSymExecMaxIters diff --git a/lib/Echidna/SymExec.hs b/lib/Echidna/SymExec.hs index 3c0b973d2..996a5cd7a 100644 --- a/lib/Echidna/SymExec.hs +++ b/lib/Echidna/SymExec.hs @@ -60,7 +60,11 @@ exploreContract conf contract tx vm = do let isConc = isJust tx allMethods = Map.elems contract.abiMap - concMethods (Tx { call = SolCall (methodName, _) }) = filter (\method -> method.name == methodName) allMethods + filterMethod name method = method.name == name && + case conf.campaignConf.symExecTargets of + Just ms -> name `elem` ms + _ -> True + concMethods (Tx { call = SolCall (methodName, _) }) = filter (filterMethod methodName) allMethods concMethods _ = error "`exploreContract` should only be called with Nothing or Just Tx{call=SolCall _} for its tx argument" methods = maybe allMethods concMethods tx timeout = Just (fromIntegral conf.campaignConf.symExecTimeout) diff --git a/lib/Echidna/Types/Campaign.hs b/lib/Echidna/Types/Campaign.hs index c0b1e3351..6a9a1521a 100644 --- a/lib/Echidna/Types/Campaign.hs +++ b/lib/Echidna/Types/Campaign.hs @@ -49,6 +49,7 @@ data CampaignConf = CampaignConf , symExecConcolic :: Bool -- ^ Whether symbolic execution will be concolic (vs full symbolic execution) -- Only relevant if symExec is True + , symExecTargets :: Maybe [Text] , symExecTimeout :: Int -- ^ Timeout for symbolic execution SMT solver. -- Only relevant if symExec is True diff --git a/src/Main.hs b/src/Main.hs index 312c805d3..36e33665c 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -140,6 +140,7 @@ data Options = Options , cliCryticArgs :: Maybe String , cliSolcArgs :: Maybe String , cliSymExec :: Maybe Bool + , cliSymExecTargets :: Maybe Text , cliSymExecTimeout :: Maybe Int , cliSymExecNSolvers :: Maybe Int } @@ -220,6 +221,9 @@ options = Options <*> optional (option bool $ long "sym-exec" <> metavar "BOOL" <> help "Whether to enable the experimental symbolic execution feature.") + <*> optional (option str $ long "sym-exec-target" + <> metavar "SELECTOR" + <> help "Target for the symbolic execution run (assuming sym-exec is enabled). Default is all functions") <*> optional (option auto $ long "sym-exec-timeout" <> metavar "INTEGER" <> help ("Timeout for each symbolic execution run, in seconds (assuming sym-exec is enabled). Default is " ++ show defaultSymExecTimeout)) @@ -268,6 +272,7 @@ overrideConfig config Options{..} = do , workers = cliWorkers <|> campaignConf.workers , serverPort = cliServerPort <|> campaignConf.serverPort , symExec = fromMaybe campaignConf.symExec cliSymExec + , symExecTargets = (\ t -> Just [t]) =<< cliSymExecTargets , symExecTimeout = fromMaybe campaignConf.symExecTimeout cliSymExecTimeout , symExecNSolvers = fromMaybe campaignConf.symExecNSolvers cliSymExecNSolvers } diff --git a/tests/solidity/basic/default.yaml b/tests/solidity/basic/default.yaml index 818156fa6..1a9458bd9 100644 --- a/tests/solidity/basic/default.yaml +++ b/tests/solidity/basic/default.yaml @@ -110,3 +110,6 @@ symExecMaxIters: 10 # Number of times we may revisit a particular branching point before we consult the smt solver to check reachability # only relevant if symExec is true and symExecConcolic is false symExecAskSMTIters: 1 +# List of whitelisted functions for using symbolic/concolic exploration +# only relevant if symExec is true +symExecTargets: null From 55a80914de235ebe06e237e0d03799fb01b231c9 Mon Sep 17 00:00:00 2001 From: Gustavo Grieco <31542053+ggrieco-tob@users.noreply.github.com> Date: Tue, 28 May 2024 19:29:12 +0200 Subject: [PATCH 05/29] Improved shrinking removing reverts from reproducers (#1250) * remove reverted sequences from reproducers * fixes * concat NoCalls * clean useless no calls when delay is zero * avoid reversing transaction sequence --- lib/Echidna/Shrink.hs | 67 ++++++++++++++++++++++++++++------------- lib/Echidna/Types/Tx.hs | 19 ++++++++++++ 2 files changed, 65 insertions(+), 21 deletions(-) diff --git a/lib/Echidna/Shrink.hs b/lib/Echidna/Shrink.hs index fb00d3e05..a8641d5a1 100644 --- a/lib/Echidna/Shrink.hs +++ b/lib/Echidna/Shrink.hs @@ -8,14 +8,15 @@ import Control.Monad.State.Strict (MonadIO) import Control.Monad.ST (RealWorld) import Data.Set qualified as Set import Data.List qualified as List +import Data.Maybe (mapMaybe) -import EVM.Types (VM, VMType(Concrete)) +import EVM.Types (VM, VMType(..)) import Echidna.Exec import Echidna.Transaction import Echidna.Types.Solidity (SolConf(..)) import Echidna.Types.Test (TestValue(..), EchidnaTest(..), TestState(..), isOptimizationTest) -import Echidna.Types.Tx (Tx(..)) +import Echidna.Types.Tx (Tx(..), hasReverted, isUselessNoCall, catNoCalls, TxCall(..)) import Echidna.Types.Config import Echidna.Types.Campaign (CampaignConf(..)) import Echidna.Test (getResultFromVM, checkETest) @@ -31,24 +32,47 @@ shrinkTest vm test = do Large i | i >= env.cfg.campaignConf.shrinkLimit && not (isOptimizationTest test) -> pure $ Just test { state = Solved } Large i -> - if length test.reproducer > 1 || any canShrinkTx test.reproducer then do - maybeShrunk <- shrinkSeq vm (checkETest test) test.value test.reproducer - pure $ case maybeShrunk of - Just (txs, val, vm') -> do - Just test { state = Large (i + 1) - , reproducer = txs - , vm = Just vm' - , result = getResultFromVM vm' - , value = val } - Nothing -> - -- No success with shrinking this time, just bump trials - Just test { state = Large (i + 1) } - else - pure $ Just test { state = if isOptimizationTest test - then Large (i + 1) - else Solved } + do repro <- removeReverts vm test.reproducer + let rr = removeUselessNoCalls $ catNoCalls repro + if length rr > 1 || any canShrinkTx rr then do + maybeShrunk <- shrinkSeq vm (checkETest test) test.value rr + pure $ case maybeShrunk of + Just (txs, val, vm') -> do + Just test { state = Large (i + 1) + , reproducer = txs + , vm = Just vm' + , result = getResultFromVM vm' + , value = val } + Nothing -> + -- No success with shrinking this time, just bump trials + Just test { state = Large (i + 1), reproducer = rr} + else + pure $ Just test { state = if isOptimizationTest test + then Large (i + 1) + else Solved } _ -> pure Nothing +replaceByNoCall :: Tx -> Tx +replaceByNoCall tx = tx { call = NoCall } + +removeUselessNoCalls :: [Tx] -> [Tx] +removeUselessNoCalls = mapMaybe f + where f tx = if isUselessNoCall tx then Nothing else Just tx + +removeReverts :: (MonadIO m, MonadReader Env m, MonadThrow m) => VM Concrete RealWorld -> [Tx] -> m [Tx] +removeReverts vm txs = do + let (itxs, le) = (init txs, last txs) + ftxs <- removeReverts' vm itxs [] + return (ftxs ++ [le]) + +removeReverts' :: (MonadIO m, MonadReader Env m, MonadThrow m) => VM Concrete RealWorld -> [Tx] -> [Tx] -> m [Tx] +removeReverts' _ [] ftxs = return $ reverse ftxs +removeReverts' vm (t:txs) ftxs = do + (_, vm') <- execTx vm t + if hasReverted vm' + then removeReverts' vm' txs (replaceByNoCall t: ftxs) + else removeReverts' vm' txs (t:ftxs) + -- | Given a call sequence that solves some Echidna test, try to randomly -- generate a smaller one that still solves that test. shrinkSeq @@ -60,11 +84,12 @@ shrinkSeq -> m (Maybe ([Tx], TestValue, VM Concrete RealWorld)) shrinkSeq vm f v txs = do txs' <- uniform =<< sequence [shorten, shrunk] - (value, vm') <- check txs' vm + let txs'' = removeUselessNoCalls txs' + (value, vm') <- check txs'' vm -- if the test passed it means we didn't shrink successfully pure $ case (value,v) of - (BoolValue False, _) -> Just (txs', value, vm') - (IntValue x, IntValue y) | x >= y -> Just (txs', value, vm') + (BoolValue False, _) -> Just (txs'', value, vm') + (IntValue x, IntValue y) | x >= y -> Just (txs'', value, vm') _ -> Nothing where check [] vm' = f vm' diff --git a/lib/Echidna/Types/Tx.hs b/lib/Echidna/Types/Tx.hs index fe820580a..11c1372cc 100644 --- a/lib/Echidna/Types/Tx.hs +++ b/lib/Echidna/Types/Tx.hs @@ -9,6 +9,7 @@ module Echidna.Types.Tx where import Prelude hiding (Word) import Control.Applicative ((<|>)) +import Control.Monad.ST (RealWorld) import Data.Aeson (FromJSON, ToJSON, parseJSON, toJSON, object, withObject, (.=), (.:)) import Data.Aeson.TH (deriveJSON, defaultOptions) import Data.Aeson.Types (Parser) @@ -199,6 +200,24 @@ data TxConf = TxConf -- ^ Maximum value to use in transactions } +hasReverted :: VM Concrete RealWorld -> Bool +hasReverted vm = let r = vm.result in + case r of + (Just (VMSuccess _)) -> False + _ -> True + +isUselessNoCall :: Tx -> Bool +isUselessNoCall tx = tx.call == NoCall && tx.delay == (0, 0) + +catNoCalls :: [Tx] -> [Tx] +catNoCalls [] = [] +catNoCalls [tx] = [tx] +catNoCalls (tx1:tx2:xs) = + case (tx1.call, tx2.call) of + (NoCall, NoCall) -> catNoCalls (nc:xs) + _ -> tx1 : catNoCalls (tx2:xs) + where nc = tx1 { delay = (fst tx1.delay + fst tx2.delay, snd tx1.delay + snd tx2.delay) } + -- | Transform a VMResult into a more hash friendly sum type getResult :: VMResult Concrete s -> TxResult getResult = \case From 42e633876a39db82f07c8813e42fb63170aaa26c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Jun 2024 12:14:51 +0200 Subject: [PATCH 06/29] Bump DeterminateSystems/nix-installer-action from 11 to 12 (#1268) Bumps [DeterminateSystems/nix-installer-action](https://github.com/determinatesystems/nix-installer-action) from 11 to 12. - [Release notes](https://github.com/determinatesystems/nix-installer-action/releases) - [Commits](https://github.com/determinatesystems/nix-installer-action/compare/v11...v12) --- updated-dependencies: - dependency-name: DeterminateSystems/nix-installer-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6c17c555d..8a5fac02d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,7 +36,7 @@ jobs: uses: actions/checkout@v4 - name: Install Nix - uses: DeterminateSystems/nix-installer-action@v11 + uses: DeterminateSystems/nix-installer-action@v12 - name: Configure Cachix uses: cachix/cachix-action@v15 From 5a366d2ea3662485c4f2c22225248755a896bb89 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Jun 2024 12:15:03 +0200 Subject: [PATCH 07/29] Bump DeterminateSystems/magic-nix-cache-action from 6 to 7 (#1267) Bumps [DeterminateSystems/magic-nix-cache-action](https://github.com/determinatesystems/magic-nix-cache-action) from 6 to 7. - [Release notes](https://github.com/determinatesystems/magic-nix-cache-action/releases) - [Commits](https://github.com/determinatesystems/magic-nix-cache-action/compare/v6...v7) --- updated-dependencies: - dependency-name: DeterminateSystems/magic-nix-cache-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8a5fac02d..405b261bc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,7 +45,7 @@ jobs: authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} - name: Configure Nix cache - uses: DeterminateSystems/magic-nix-cache-action@v6 + uses: DeterminateSystems/magic-nix-cache-action@v7 with: upstream-cache: https://trailofbits.cachix.org From 8347ad91c3fd5bc2d322c40f117799b558eab660 Mon Sep 17 00:00:00 2001 From: Artur Cygan Date: Thu, 6 Jun 2024 14:36:39 +0200 Subject: [PATCH 08/29] Improve max code size error message (#1269) * Improve max code size error message * Change codeSize default to 0xffffffff --- lib/Echidna/Config.hs | 2 +- lib/Echidna/Types.hs | 15 ++++++++++++--- lib/Echidna/Types/Solidity.hs | 2 +- tests/solidity/basic/default.yaml | 4 ++-- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/lib/Echidna/Config.hs b/lib/Echidna/Config.hs index cbe6ca17e..ed7796014 100644 --- a/lib/Echidna/Config.hs +++ b/lib/Echidna/Config.hs @@ -112,7 +112,7 @@ instance FromJSON EConfigWithUsage where <*> v ..:? "sender" ..!= Set.fromList [0x10000, 0x20000, defaultDeployerAddr] <*> v ..:? "balanceAddr" ..!= 0xffffffff <*> v ..:? "balanceContract" ..!= 0 - <*> v ..:? "codeSize" ..!= 0x6000 -- 24576 (EIP-170) + <*> v ..:? "codeSize" ..!= 0xffffffff <*> v ..:? "prefix" ..!= "echidna_" <*> v ..:? "cryticArgs" ..!= [] <*> v ..:? "solcArgs" ..!= "" diff --git a/lib/Echidna/Types.hs b/lib/Echidna/Types.hs index e2eccf62a..316d49e83 100644 --- a/lib/Echidna/Types.hs +++ b/lib/Echidna/Types.hs @@ -11,9 +11,18 @@ import EVM.Types data ExecException = IllegalExec EvmError | UnknownFailure EvmError instance Show ExecException where - show (IllegalExec e) = "VM attempted an illegal operation: " ++ show e - show (UnknownFailure e) = "VM failed for unhandled reason, " ++ show e - ++ ". This shouldn't happen. Please file a ticket with this error message and steps to reproduce!" + show = \case + IllegalExec e -> "VM attempted an illegal operation: " ++ show e + UnknownFailure (MaxCodeSizeExceeded limit actual) -> + "Max code size exceeded. " ++ codeSizeErrorDetails limit actual + UnknownFailure (MaxInitCodeSizeExceeded limit actual) -> + "Max init code size exceeded. " ++ codeSizeErrorDetails limit actual + UnknownFailure e -> "VM failed for unhandled reason, " ++ show e + ++ ". This shouldn't happen. Please file a ticket with this error message and steps to reproduce!" + where + codeSizeErrorDetails limit actual = + "Configured limit: " ++ show limit ++ ", actual: " ++ show actual + ++ ". Set 'codeSize: 0xffffffff' in the config file to increase the limit." instance Exception ExecException diff --git a/lib/Echidna/Types/Solidity.hs b/lib/Echidna/Types/Solidity.hs index 792c1b269..f17e81697 100644 --- a/lib/Echidna/Types/Solidity.hs +++ b/lib/Echidna/Types/Solidity.hs @@ -64,7 +64,7 @@ data SolConf = SolConf , sender :: Set Addr -- ^ Sender addresses to use , balanceAddr :: Integer -- ^ Initial balance of deployer and senders , balanceContract :: Integer -- ^ Initial balance of contract to test - , codeSize :: Integer -- ^ Max code size for deployed contratcs (default 24576, per EIP-170) + , codeSize :: Integer -- ^ Max code size for deployed contratcs (default 0xffffffff) , prefix :: Text -- ^ Function name prefix used to denote tests , cryticArgs :: [String] -- ^ Args to pass to crytic , solcArgs :: String -- ^ Args to pass to @solc@ diff --git a/tests/solidity/basic/default.yaml b/tests/solidity/basic/default.yaml index 1a9458bd9..36428ac49 100644 --- a/tests/solidity/basic/default.yaml +++ b/tests/solidity/basic/default.yaml @@ -38,8 +38,8 @@ sender: ["0x10000", "0x20000", "0x30000"] balanceAddr: 0xffffffff #balanceContract overrides balanceAddr for the contract address balanceContract: 0 -#codeSize max code size for deployed contratcs (default 24576, per EIP-170) -codeSize: 0x6000 +#codeSize max code size for deployed contratcs (default 0xffffffff) +codeSize: 0xffffffff #solcArgs allows special args to solc solcArgs: "" #solcLibs is solc libraries From 9f05340c1280c723c33085b6a96061a5680d30c7 Mon Sep 17 00:00:00 2001 From: samalws-tob <129795909+samalws-tob@users.noreply.github.com> Date: Thu, 13 Jun 2024 05:18:40 -0400 Subject: [PATCH 09/29] show transactions when test is falsified in text mode (#1271) --- lib/Echidna/UI.hs | 2 +- lib/Echidna/UI/Report.hs | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/Echidna/UI.hs b/lib/Echidna/UI.hs index 7ed7af724..000d87527 100644 --- a/lib/Echidna/UI.hs +++ b/lib/Echidna/UI.hs @@ -173,7 +173,7 @@ ui vm world dict initialCorpus cliSelectedContract cs = do void $ tryPutMVar serverStopVar () in installHandler sig handler Nothing #endif - let forwardEvent = putStrLn . ppLogLine + let forwardEvent ev = putStrLn =<< runReaderT (ppLogLine vm ev) env uiEventsForwarderStopVar <- spawnListener forwardEvent let printStatus = do diff --git a/lib/Echidna/UI/Report.hs b/lib/Echidna/UI/Report.hs index d35b31da9..fa6d47b3e 100644 --- a/lib/Echidna/UI/Report.hs +++ b/lib/Echidna/UI/Report.hs @@ -29,13 +29,19 @@ import EVM.Format (showTraceTree, contractNamePart) import EVM.Solidity (SolcContract(..)) import EVM.Types (W256, VM, VMType(Concrete), Addr, Expr (LitAddr)) -ppLogLine :: (LocalTime, CampaignEvent) -> String -ppLogLine (time, event@(WorkerEvent workerId FuzzWorker _)) = - timePrefix time <> "[Worker " <> show workerId <> "] " <> ppCampaignEvent event -ppLogLine (time, event@(WorkerEvent workerId SymbolicWorker _)) = - timePrefix time <> "[Worker " <> show workerId <> ", symbolic] " <> ppCampaignEvent event -ppLogLine (time, event) = - timePrefix time <> " " <> ppCampaignEvent event +ppLogLine :: MonadReader Env m => VM Concrete RealWorld -> (LocalTime, CampaignEvent) -> m String +ppLogLine vm (time, event@(WorkerEvent workerId FuzzWorker _)) = + ((timePrefix time <> "[Worker " <> show workerId <> "] ") <>) <$> ppCampaignEventLog vm event +ppLogLine vm (time, event@(WorkerEvent workerId SymbolicWorker _)) = + ((timePrefix time <> "[Worker " <> show workerId <> ", symbolic] ") <>) <$> ppCampaignEventLog vm event +ppLogLine vm (time, event) = + ((timePrefix time <> " ") <>) <$> ppCampaignEventLog vm event + +ppCampaignEventLog :: MonadReader Env m => VM Concrete RealWorld -> CampaignEvent -> m String +ppCampaignEventLog vm ev = (ppCampaignEvent ev <>) <$> ppTxIfHas where + ppTxIfHas = case ev of + (WorkerEvent _ _ (TestFalsified test)) -> ("\n Call sequence:\n" <>) . unlines <$> mapM (ppTx vm $ length (nub $ (.src) <$> test.reproducer) /= 1) test.reproducer + _ -> pure "" ppCampaign :: (MonadIO m, MonadReader Env m) => VM Concrete RealWorld -> [WorkerState] -> m String ppCampaign vm workerStates = do From 1cb47b37d282a999395e56cb8991bf28fbd09edb Mon Sep 17 00:00:00 2001 From: samalws-tob <129795909+samalws-tob@users.noreply.github.com> Date: Thu, 13 Jun 2024 05:19:11 -0400 Subject: [PATCH 10/29] emit log message when saving reproducers (#1273) --- lib/Echidna/Output/Corpus.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Echidna/Output/Corpus.hs b/lib/Echidna/Output/Corpus.hs index 258041197..21e06e1c4 100644 --- a/lib/Echidna/Output/Corpus.hs +++ b/lib/Echidna/Output/Corpus.hs @@ -23,6 +23,7 @@ saveTxs dir = mapM_ saveTxSeq where saveTxSeq txSeq = do createDirectoryIfMissing True dir let file = dir (show . abs . hash . show) txSeq <.> "txt" + putStrLn ("Saving reproducer to " ++ file) unlessM (doesFileExist file) $ encodeFile file (toJSON txSeq) loadTxs :: FilePath -> IO [(FilePath, [Tx])] From a73bc9d0c075804bfd4044cfd21429408edf4677 Mon Sep 17 00:00:00 2001 From: cangqiaoyuzhuo <850072022@qq.com> Date: Fri, 14 Jun 2024 22:20:05 +0900 Subject: [PATCH 11/29] chore: fix some comments (#1272) Signed-off-by: cangqiaoyuzhuo <850072022@qq.com> --- lib/Echidna/SymExec.hs | 2 +- .../TestAddressArrayUtils.sol | 50 +++++++++---------- .../TestAddressArrayUtilsRevert.sol | 50 +++++++++---------- 3 files changed, 51 insertions(+), 51 deletions(-) diff --git a/lib/Echidna/SymExec.hs b/lib/Echidna/SymExec.hs index 996a5cd7a..2c192abc7 100644 --- a/lib/Echidna/SymExec.hs +++ b/lib/Echidna/SymExec.hs @@ -229,7 +229,7 @@ substExpr (sw, sa, val) = mapExpr go where go TxValue = Lit val go e = e --- | Fetcher used during concolic exeuction. +-- | Fetcher used during concolic execution. -- This is the most important function for concolic execution; -- it determines what branch `interpret` should take. -- We ensure that this fetcher is always used by setting askSMTIter to 0. diff --git a/tests/solidity/addressarrayutils/TestAddressArrayUtils.sol b/tests/solidity/addressarrayutils/TestAddressArrayUtils.sol index 63470ad22..1667c60da 100644 --- a/tests/solidity/addressarrayutils/TestAddressArrayUtils.sol +++ b/tests/solidity/addressarrayutils/TestAddressArrayUtils.sol @@ -329,24 +329,24 @@ contract TEST { if (!AddressArrayUtils.contains(addrs1, a)) { return true; } - uint256 acount = 0; + uint256 account = 0; uint256 i; for (i = 0; i < addrs1.length; i++) { if (addrs1[i] == a) { - acount++; + account++; } } address [] memory removed = AddressArrayUtils.remove(addrs1, a); if (removed.length != (addrs1.length-1)) { return false; } - uint256 acountNew = 0; + uint256 accountNew = 0; for (i = 0; i < removed.length; i++) { if (removed[i] == a) { - acountNew++; + accountNew++; } } - if (acountNew != (acount-1)) { + if (accountNew != (account-1)) { return false; } if (!AddressArrayUtils.hasDuplicate(addrs1)) { @@ -367,11 +367,11 @@ contract TEST { uint256 aIndex; bool aFound; (aIndex, aFound) = AddressArrayUtils.indexOf(addrs1, a); - uint256 acount = 0; + uint256 account = 0; uint256 i; for (i = 0; i < addrs1.length; i++) { if (addrs1[i] == a) { - acount++; + account++; } } address [] memory removed; @@ -383,13 +383,13 @@ contract TEST { if (removed.length != (addrs1.length-1)) { return false; } - uint256 acountNew = 0; + uint256 accountNew = 0; for (i = 0; i < removed.length; i++) { if (removed[i] == a) { - acountNew++; + accountNew++; } } - if (acountNew != (acount-1)) { + if (accountNew != (account-1)) { return false; } if (!AddressArrayUtils.hasDuplicate(addrs1)) { @@ -407,11 +407,11 @@ contract TEST { if (!AddressArrayUtils.contains(addrs1, a)) { return true; } - uint256 acount = 0; + uint256 account = 0; uint256 i; for (i = 0; i < addrs1.length; i++) { if (addrs1[i] == a) { - acount++; + account++; } } uint256 oldLength = addrs1.length; @@ -420,13 +420,13 @@ contract TEST { if (addrs1.length != (oldLength-1)) { return false; } - uint256 acountNew = 0; + uint256 accountNew = 0; for (i = 0; i < addrs1.length; i++) { if (addrs1[i] == a) { - acountNew++; + accountNew++; } } - if (acountNew != (acount-1)) { + if (accountNew != (account-1)) { return false; } if (!anyDuplicates) { @@ -447,11 +447,11 @@ contract TEST { uint256 aIndex; bool aFound; (aIndex, aFound) = AddressArrayUtils.indexOf(addrs1, a); - uint256 acount = 0; + uint256 account = 0; uint256 i; for (i = 0; i < addrs1.length; i++) { if (addrs1[i] == a) { - acount++; + account++; } } uint256 oldLength = addrs1.length; @@ -463,13 +463,13 @@ contract TEST { if (addrs1.length != (oldLength-1)) { return false; } - uint256 acountNew = 0; + uint256 accountNew = 0; for (i = 0; i < addrs1.length; i++) { if (addrs1[i] == a) { - acountNew++; + accountNew++; } } - if (acountNew != (acount-1)) { + if (accountNew != (account-1)) { return false; } if (!anyDuplicates) { @@ -490,11 +490,11 @@ contract TEST { uint256 aIndex; bool aFound; (aIndex, aFound) = AddressArrayUtils.indexOf(addrs1, a); - uint256 acount = 0; + uint256 account = 0; uint256 i; for (i = 0; i < addrs1.length; i++) { if (addrs1[i] == a) { - acount++; + account++; } } uint256 oldLength = addrs1.length; @@ -506,13 +506,13 @@ contract TEST { if (addrs1.length != (oldLength-1)) { return false; } - uint256 acountNew = 0; + uint256 accountNew = 0; for (i = 0; i < addrs1.length; i++) { if (addrs1[i] == a) { - acountNew++; + accountNew++; } } - if (acountNew != (acount-1)) { + if (accountNew != (account-1)) { return false; } if (!anyDuplicates) { diff --git a/tests/solidity/addressarrayutils/TestAddressArrayUtilsRevert.sol b/tests/solidity/addressarrayutils/TestAddressArrayUtilsRevert.sol index 079d7ecb4..abf690404 100644 --- a/tests/solidity/addressarrayutils/TestAddressArrayUtilsRevert.sol +++ b/tests/solidity/addressarrayutils/TestAddressArrayUtilsRevert.sol @@ -329,24 +329,24 @@ contract TEST { if (!AddressArrayUtils.contains(addrs1, a)) { return true; } - uint256 acount = 0; + uint256 account = 0; uint256 i; for (i = 0; i < addrs1.length; i++) { if (addrs1[i] == a) { - acount++; + account++; } } address [] memory removed = AddressArrayUtils.remove(addrs1, a); if (removed.length != (addrs1.length-1)) { return false; } - uint256 acountNew = 0; + uint256 accountNew = 0; for (i = 0; i < removed.length; i++) { if (removed[i] == a) { - acountNew++; + accountNew++; } } - if (acountNew != (acount-1)) { + if (accountNew != (account-1)) { return false; } if (!AddressArrayUtils.hasDuplicate(addrs1)) { @@ -377,11 +377,11 @@ contract TEST { uint256 aIndex; bool aFound; (aIndex, aFound) = AddressArrayUtils.indexOf(addrs1, a); - uint256 acount = 0; + uint256 account = 0; uint256 i; for (i = 0; i < addrs1.length; i++) { if (addrs1[i] == a) { - acount++; + account++; } } address [] memory removed; @@ -393,13 +393,13 @@ contract TEST { if (removed.length != (addrs1.length-1)) { return false; } - uint256 acountNew = 0; + uint256 accountNew = 0; for (i = 0; i < removed.length; i++) { if (removed[i] == a) { - acountNew++; + accountNew++; } } - if (acountNew != (acount-1)) { + if (accountNew != (account-1)) { return false; } if (!AddressArrayUtils.hasDuplicate(addrs1)) { @@ -417,11 +417,11 @@ contract TEST { if (!AddressArrayUtils.contains(addrs1, a)) { return true; } - uint256 acount = 0; + uint256 account = 0; uint256 i; for (i = 0; i < addrs1.length; i++) { if (addrs1[i] == a) { - acount++; + account++; } } uint256 oldLength = addrs1.length; @@ -430,13 +430,13 @@ contract TEST { if (addrs1.length != (oldLength-1)) { return false; } - uint256 acountNew = 0; + uint256 accountNew = 0; for (i = 0; i < addrs1.length; i++) { if (addrs1[i] == a) { - acountNew++; + accountNew++; } } - if (acountNew != (acount-1)) { + if (accountNew != (account-1)) { return false; } if (!anyDuplicates) { @@ -467,11 +467,11 @@ contract TEST { uint256 aIndex; bool aFound; (aIndex, aFound) = AddressArrayUtils.indexOf(addrs1, a); - uint256 acount = 0; + uint256 account = 0; uint256 i; for (i = 0; i < addrs1.length; i++) { if (addrs1[i] == a) { - acount++; + account++; } } uint256 oldLength = addrs1.length; @@ -483,13 +483,13 @@ contract TEST { if (addrs1.length != (oldLength-1)) { return false; } - uint256 acountNew = 0; + uint256 accountNew = 0; for (i = 0; i < addrs1.length; i++) { if (addrs1[i] == a) { - acountNew++; + accountNew++; } } - if (acountNew != (acount-1)) { + if (accountNew != (account-1)) { return false; } if (!anyDuplicates) { @@ -518,11 +518,11 @@ contract TEST { uint256 aIndex; bool aFound; (aIndex, aFound) = AddressArrayUtils.indexOf(addrs1, a); - uint256 acount = 0; + uint256 account = 0; uint256 i; for (i = 0; i < addrs1.length; i++) { if (addrs1[i] == a) { - acount++; + account++; } } uint256 oldLength = addrs1.length; @@ -534,13 +534,13 @@ contract TEST { if (addrs1.length != (oldLength-1)) { return false; } - uint256 acountNew = 0; + uint256 accountNew = 0; for (i = 0; i < addrs1.length; i++) { if (addrs1[i] == a) { - acountNew++; + accountNew++; } } - if (acountNew != (acount-1)) { + if (accountNew != (account-1)) { return false; } if (!anyDuplicates) { From bd90027726ca091f8d6755fec1fb17b13a562f14 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Jun 2024 12:17:19 +0200 Subject: [PATCH 12/29] Bump docker/build-push-action from 5 to 6 (#1275) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v5...v6) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 7ad285eb7..9d09f5f1c 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -61,7 +61,7 @@ jobs: password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }} - name: Docker Build and Push (Ubuntu & NVM variant) - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: platforms: linux/amd64 target: final-ubuntu @@ -74,7 +74,7 @@ jobs: cache-to: type=gha,mode=max - name: Docker Build and Push (Distroless variant) - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 if: ${{ env.WORKFLOW_BUILD_DISTROLESS == true }} with: platforms: linux/amd64 From 7953bd7a9de90d05bac8397349d150726723eab4 Mon Sep 17 00:00:00 2001 From: samalws-tob <129795909+samalws-tob@users.noreply.github.com> Date: Fri, 21 Jun 2024 07:56:20 -0400 Subject: [PATCH 13/29] emit saved reproducer log message as event rather than putstrln (#1274) --- lib/Echidna/Output/Corpus.hs | 8 ++++---- lib/Echidna/Server.hs | 5 +++++ lib/Echidna/Types/Campaign.hs | 2 ++ src/Main.hs | 4 ++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/Echidna/Output/Corpus.hs b/lib/Echidna/Output/Corpus.hs index 21e06e1c4..4151d14b3 100644 --- a/lib/Echidna/Output/Corpus.hs +++ b/lib/Echidna/Output/Corpus.hs @@ -18,13 +18,13 @@ import Echidna.Types.Test (EchidnaTest(..)) import Echidna.Types.Tx (Tx) import Echidna.Utility (listDirectory, withCurrentDirectory) -saveTxs :: FilePath -> [[Tx]] -> IO () -saveTxs dir = mapM_ saveTxSeq where +saveTxs :: Env -> FilePath -> [[Tx]] -> IO () +saveTxs env dir = mapM_ saveTxSeq where saveTxSeq txSeq = do createDirectoryIfMissing True dir let file = dir (show . abs . hash . show) txSeq <.> "txt" - putStrLn ("Saving reproducer to " ++ file) unlessM (doesFileExist file) $ encodeFile file (toJSON txSeq) + pushCampaignEvent env (ReproducerSaved file) loadTxs :: FilePath -> IO [(FilePath, [Tx])] loadTxs dir = do @@ -59,7 +59,7 @@ saveCorpusEvent env (_time, campaignEvent) = do saveFile dir (subdir, txs) = unless (null txs) $ - handle exceptionHandler $ saveTxs (dir subdir) [txs] + handle exceptionHandler $ saveTxs env (dir subdir) [txs] exceptionHandler (e :: IOException) = pushCampaignEvent env (Failure $ "Problem while writing to file: " ++ show e) diff --git a/lib/Echidna/Server.hs b/lib/Echidna/Server.hs index 73d4592f6..6827f790c 100644 --- a/lib/Echidna/Server.hs +++ b/lib/Echidna/Server.hs @@ -30,6 +30,10 @@ instance ToJSON SSE where object [ "timestamp" .= time , "data" .= reason ] + toJSON (SSE (time, ReproducerSaved filename)) = + object [ "timestamp" .= time + , "filename" .= filename + ] runSSEServer :: MVar () -> Env -> Word16 -> Int -> IO () runSSEServer serverStopVar env port nworkers = do @@ -53,6 +57,7 @@ runSSEServer serverStopVar env port nworkers = do TxSequenceReplayFailed {} -> "tx_sequence_replay_failed" WorkerStopped _ -> "worker_stopped" Failure _err -> "failure" + ReproducerSaved _ -> "saved_reproducer" case campaignEvent of WorkerEvent _ _ (WorkerStopped _) -> do aliveAfter <- atomicModifyIORef' aliveRef (\n -> (n-1, n-1)) diff --git a/lib/Echidna/Types/Campaign.hs b/lib/Echidna/Types/Campaign.hs index 6a9a1521a..b59815bd3 100644 --- a/lib/Echidna/Types/Campaign.hs +++ b/lib/Echidna/Types/Campaign.hs @@ -72,6 +72,7 @@ type WorkerId = Int data CampaignEvent = WorkerEvent WorkerId WorkerType WorkerEvent | Failure String + | ReproducerSaved String -- filename data WorkerEvent = TestFalsified !EchidnaTest @@ -111,6 +112,7 @@ ppCampaignEvent :: CampaignEvent -> String ppCampaignEvent = \case WorkerEvent _ _ e -> ppWorkerEvent e Failure err -> err + ReproducerSaved f -> "Saved reproducer to " <> f ppWorkerEvent :: WorkerEvent -> String ppWorkerEvent = \case diff --git a/src/Main.hs b/src/Main.hs index 36e33665c..d64f4023a 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -81,7 +81,7 @@ main = withUtf8 $ withCP65001 $ do Nothing -> pure () Just dir -> do measureIO cfg.solConf.quiet "Saving test reproducers" $ - saveTxs (dir "reproducers") (filter (not . null) $ (.reproducer) <$> tests) + saveTxs env (dir "reproducers") (filter (not . null) $ (.reproducer) <$> tests) saveTracesEnabled <- lookupEnv "ECHIDNA_SAVE_TRACES" when (isJust saveTracesEnabled) $ do @@ -98,7 +98,7 @@ main = withUtf8 $ withCP65001 $ do measureIO cfg.solConf.quiet "Saving corpus" $ do corpus <- readIORef env.corpusRef - saveTxs (dir "coverage") (snd <$> Set.toList corpus) + saveTxs env (dir "coverage") (snd <$> Set.toList corpus) -- TODO: We use the corpus dir to save coverage reports which is confusing. -- Add config option to pass dir for saving coverage report and decouple it From 2c72579e72eb2818d63c60a11180e7359f9984e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Jun 2024 13:28:20 +0200 Subject: [PATCH 14/29] Bump softprops/action-gh-release from 2.0.5 to 2.0.6 (#1277) Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.0.5 to 2.0.6. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/v2.0.5...v2.0.6) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 405b261bc..d38832264 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -101,7 +101,7 @@ jobs: inputs: ./echidna-*.tar.gz - name: Create GitHub release and upload binaries - uses: softprops/action-gh-release@v2.0.5 + uses: softprops/action-gh-release@v2.0.6 with: draft: true name: "Echidna ${{ needs.nixBuild.outputs.version }}" From 882c6994d244ea38656c6c90fbf2bf1e3981d2b3 Mon Sep 17 00:00:00 2001 From: samalws-tob <129795909+samalws-tob@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:49:59 -0400 Subject: [PATCH 15/29] show gas/s (#1279) --- lib/Echidna/Campaign.hs | 3 +++ lib/Echidna/Types/Campaign.hs | 3 +++ lib/Echidna/UI/Widgets.hs | 14 ++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/lib/Echidna/Campaign.hs b/lib/Echidna/Campaign.hs index 1b07cfa1e..83e522733 100644 --- a/lib/Echidna/Campaign.hs +++ b/lib/Echidna/Campaign.hs @@ -137,6 +137,7 @@ runSymWorker callback vm dict workerId initialCorpus name cs = do , newCoverage = False , ncallseqs = 0 , ncalls = 0 + , totalGas = 0 , runningThreads = [] } @@ -213,6 +214,7 @@ runFuzzWorker callback vm world dict workerId initialCorpus testLimit = do , newCoverage = False , ncallseqs = 0 , ncalls = 0 + , totalGas = 0 , runningThreads = [] } @@ -453,6 +455,7 @@ evalSeq vm0 execFunc = go vm0 [] where [] -> pure ([], vm) (tx:remainingTxs) -> do (result, vm') <- execFunc vm tx + modify' $ \workerState -> workerState { totalGas = workerState.totalGas + fromIntegral (vm'.burned - vm.burned) } -- NOTE: we don't use the intermediate VMs, just the last one. If any of -- the intermediate VMs are needed, they can be put next to the result -- of each transaction - `m ([(Tx, result, VM)])` diff --git a/lib/Echidna/Types/Campaign.hs b/lib/Echidna/Types/Campaign.hs index b59815bd3..158b69f74 100644 --- a/lib/Echidna/Types/Campaign.hs +++ b/lib/Echidna/Types/Campaign.hs @@ -168,6 +168,8 @@ data WorkerState = WorkerState -- ^ Number of times the callseq is called , ncalls :: !Int -- ^ Number of calls executed while fuzzing + , totalGas :: !Int + -- ^ Total gas consumed while fuzzing , runningThreads :: [ThreadId] -- ^ Extra threads currently being run, -- aside from the main worker thread @@ -181,6 +183,7 @@ initialWorkerState = , newCoverage = False , ncallseqs = 0 , ncalls = 0 + , totalGas = 0 , runningThreads = [] } diff --git a/lib/Echidna/UI/Widgets.hs b/lib/Echidna/UI/Widgets.hs index 906b1a3a6..6426ef848 100644 --- a/lib/Echidna/UI/Widgets.hs +++ b/lib/Echidna/UI/Widgets.hs @@ -173,6 +173,8 @@ summaryWidget env uiState = <=> perfWidget uiState <=> + gasPerfWidget uiState + <=> str ("Total calls: " <> progress (sum $ (.ncalls) <$> uiState.campaigns) env.cfg.campaignConf.testLimit) middle = @@ -234,6 +236,18 @@ perfWidget uiState = diffLocalTime (fromMaybe uiState.now uiState.timeStopped) uiState.timeStarted +gasPerfWidget :: UIState -> Widget n +gasPerfWidget uiState = + str $ "Gas/s: " <> + if totalTime > 0 + then show $ totalGas `div` totalTime + else "-" + where + totalGas = sum $ (.totalGas) <$> uiState.campaigns + totalTime = round $ + diffLocalTime (fromMaybe uiState.now uiState.timeStopped) + uiState.timeStarted + ppSeed :: [WorkerState] -> String ppSeed campaigns = show (head campaigns).genDict.defSeed From 43ae84e3a764f222fdcf0877518e949d012f3017 Mon Sep 17 00:00:00 2001 From: samalws-tob <129795909+samalws-tob@users.noreply.github.com> Date: Fri, 5 Jul 2024 07:33:20 -0400 Subject: [PATCH 16/29] Fix MVar issue (#1281) --- lib/Echidna/UI.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Echidna/UI.hs b/lib/Echidna/UI.hs index 000d87527..8e12e90e4 100644 --- a/lib/Echidna/UI.hs +++ b/lib/Echidna/UI.hs @@ -99,7 +99,7 @@ ui vm world dict initialCorpus cliSelectedContract cs = do Interactive -> do -- Channel to push events to update UI uiChannel <- liftIO $ newBChan 1000 - let forwardEvent = writeBChan uiChannel . EventReceived + let forwardEvent = void . writeBChanNonBlocking uiChannel . EventReceived uiEventsForwarderStopVar <- spawnListener forwardEvent ticker <- liftIO . forkIO . forever $ do From ec5a3c13f92f8585a4f44454a8890fe5bdebb31a Mon Sep 17 00:00:00 2001 From: Artur Cygan Date: Fri, 5 Jul 2024 15:16:51 +0200 Subject: [PATCH 17/29] Shrink on one worker (#1280) --- lib/Echidna.hs | 64 ++++++------ lib/Echidna/Campaign.hs | 192 +++++++++++++++++++++--------------- lib/Echidna/Onchain.hs | 20 ++-- lib/Echidna/Output/JSON.hs | 2 +- lib/Echidna/Solidity.hs | 173 +++++++++++++++++--------------- lib/Echidna/Test.hs | 5 +- lib/Echidna/Types/Config.hs | 4 +- lib/Echidna/Types/Test.hs | 2 + lib/Echidna/UI.hs | 12 +-- lib/Echidna/UI/Report.hs | 2 +- lib/Echidna/UI/Widgets.hs | 69 ++++++------- src/Main.hs | 14 +-- src/test/Common.hs | 46 ++++++--- src/test/Tests/Compile.hs | 8 +- src/test/Tests/Seed.hs | 2 +- 15 files changed, 343 insertions(+), 272 deletions(-) diff --git a/lib/Echidna.hs b/lib/Echidna.hs index 2eabd85e2..64b84fe93 100644 --- a/lib/Echidna.hs +++ b/lib/Echidna.hs @@ -3,7 +3,7 @@ module Echidna where import Control.Concurrent (newChan) import Control.Monad.Catch (MonadThrow(..)) import Control.Monad.ST (RealWorld) -import Data.IORef (writeIORef, newIORef) +import Data.IORef (newIORef) import Data.List (find) import Data.List.NonEmpty (NonEmpty) import Data.List.NonEmpty qualified as NE @@ -13,25 +13,26 @@ import System.FilePath (()) import EVM (cheatCode) import EVM.ABI (AbiValue(AbiAddress)) -import EVM.Dapp (DappInfo(..), dappInfo) +import EVM.Dapp (dappInfo) import EVM.Fetch qualified -import EVM.Solidity (BuildOutput) +import EVM.Solidity (BuildOutput(..), Contracts(Contracts)) import EVM.Types hiding (Env) import Echidna.ABI import Echidna.Etheno (loadEtheno, extractFromEtheno) +import Echidna.Onchain as Onchain import Echidna.Output.Corpus import Echidna.SourceAnalysis.Slither import Echidna.Solidity import Echidna.Symbolic (forceAddr) -import Echidna.Test (createTests) import Echidna.Types.Campaign import Echidna.Types.Config import Echidna.Types.Random -import Echidna.Types.Signature import Echidna.Types.Solidity import Echidna.Types.Tx import Echidna.Types.World +import Echidna.Types.Test (EchidnaTest) +import Echidna.Types.Signature (ContractName) -- | This function is used to prepare, process, compile and initialize smart contracts for testing. -- It takes: @@ -45,17 +46,20 @@ import Echidna.Types.World -- * A list of Echidna tests to check -- * A prepopulated dictionary prepareContract - :: Env + :: EConfig -> NonEmpty FilePath + -> BuildOutput -> Maybe ContractName -> Seed - -> IO (VM Concrete RealWorld, World, GenDict) -prepareContract env solFiles specifiedContract seed = do - let solConf = env.cfg.solConf - contracts = Map.elems env.dapp.solcByName + -> IO (VM Concrete RealWorld, Env, GenDict) +prepareContract cfg solFiles buildOutput selectedContract seed = do + let solConf = cfg.solConf + (Contracts contractMap) = buildOutput.contracts + contracts = Map.elems contractMap - -- deploy contracts - (vm, funs, testNames, signatureMap) <- loadSpecified env specifiedContract contracts + mainContract <- selectMainContract solConf selectedContract contracts + tests <- mkTests solConf mainContract + signatureMap <- mkSignatureMap solConf mainContract contracts -- run processors slitherInfo <- runSlither (NE.head solFiles) solConf @@ -64,16 +68,14 @@ prepareContract env solFiles specifiedContract seed = do Just version -> throwM $ OutdatedSolcVersion version Nothing -> pure () - let - -- load tests - echidnaTests = createTests solConf.testMode - solConf.testDestruction - testNames - (forceAddr vm.state.contract) - funs + let world = mkWorld cfg.solConf signatureMap selectedContract slitherInfo contracts - world = mkWorld solConf signatureMap specifiedContract slitherInfo contracts + env <- mkEnv cfg buildOutput tests world + -- deploy contracts + vm <- loadSpecified env mainContract contracts + + let deployedAddresses = Set.fromList $ AbiAddress . forceAddr <$> Map.keys vm.env.contracts constants = enhanceConstants slitherInfo <> timeConstants @@ -88,13 +90,12 @@ prepareContract env solFiles specifiedContract seed = do seed (returnTypes contracts) - writeIORef env.testsRef echidnaTests - pure (vm, world, dict) + pure (vm, env, dict) -loadInitialCorpus :: Env -> World -> IO [(FilePath, [Tx])] -loadInitialCorpus env world = do +loadInitialCorpus :: Env -> IO [(FilePath, [Tx])] +loadInitialCorpus env = do -- load transactions from init sequence (if any) - let sigs = Set.fromList $ concatMap NE.toList (Map.elems world.highSignatureMap) + let sigs = Set.fromList $ concatMap NE.toList (Map.elems env.world.highSignatureMap) ethenoCorpus <- case env.cfg.solConf.initialize of Nothing -> pure [] @@ -112,18 +113,19 @@ loadInitialCorpus env world = do pure $ persistedCorpus ++ ethenoCorpus -mkEnv :: EConfig -> BuildOutput -> IO Env -mkEnv cfg buildOutput = do - fetchContractCache <- newIORef mempty - fetchSlotCache <- newIORef mempty +mkEnv :: EConfig -> BuildOutput -> [EchidnaTest] -> World -> IO Env +mkEnv cfg buildOutput tests world = do codehashMap <- newIORef mempty chainId <- maybe (pure Nothing) EVM.Fetch.fetchChainIdFrom cfg.rpcUrl eventQueue <- newChan coverageRef <- newIORef mempty corpusRef <- newIORef mempty - testsRef <- newIORef mempty + testRefs <- traverse newIORef tests + (contractCache, slotCache) <- Onchain.loadRpcCache cfg + fetchContractCache <- newIORef contractCache + fetchSlotCache <- newIORef slotCache -- TODO put in real path let dapp = dappInfo "/" buildOutput pure $ Env { cfg, dapp, codehashMap, fetchContractCache, fetchSlotCache - , chainId, eventQueue, coverageRef, corpusRef, testsRef + , chainId, eventQueue, coverageRef, corpusRef, testRefs, world } diff --git a/lib/Echidna/Campaign.hs b/lib/Echidna/Campaign.hs index 83e522733..8842dafb1 100644 --- a/lib/Echidna/Campaign.hs +++ b/lib/Echidna/Campaign.hs @@ -15,12 +15,12 @@ import Control.Monad.ST (RealWorld) import Control.Monad.Trans (lift) import Data.Binary.Get (runGetOrFail) import Data.ByteString.Lazy qualified as LBS -import Data.IORef (readIORef, atomicModifyIORef') +import Data.IORef (readIORef, atomicModifyIORef', writeIORef) import Data.Foldable (foldlM) import Data.List qualified as List import Data.Map qualified as Map import Data.Map (Map, (\\)) -import Data.Maybe (isJust, mapMaybe, fromMaybe) +import Data.Maybe (isJust, mapMaybe) import Data.Set (Set) import Data.Set qualified as Set import Data.Text (Text) @@ -29,7 +29,7 @@ import System.Random (mkStdGen) import EVM (cheatCode) import EVM.ABI (getAbi, AbiType(AbiAddressType), AbiValue(AbiAddress)) -import EVM.Solidity (SolcContract) +import EVM.Dapp (DappInfo(..)) import EVM.Types hiding (Env, Frame(state), Gas) import Echidna.ABI @@ -49,7 +49,6 @@ import Echidna.Types.Signature (FunctionName) import Echidna.Types.Test import Echidna.Types.Test qualified as Test import Echidna.Types.Tx (TxCall(..), Tx(..), call) -import Echidna.Types.World (World) import Echidna.Utility (getTimestamp) instance MonadThrow m => MonadThrow (RandT g m) where @@ -87,17 +86,17 @@ runWorker -> StateT WorkerState m () -- ^ Callback to run after each state update (for instrumentation) -> VM Concrete RealWorld -- ^ Initial VM state - -> World -- ^ Initial world state -> GenDict -- ^ Generation dictionary -> Int -- ^ Worker id starting from 0 -> [(FilePath, [Tx])] -- ^ Initial corpus of transactions -> Int -- ^ Test limit for this worker -> Maybe Text -- ^ Specified contract name - -> [SolcContract] -- ^ List of contracts -> m (WorkerStopReason, WorkerState) -runWorker SymbolicWorker callback vm _ dict workerId initialCorpus _ name cs = runSymWorker callback vm dict workerId initialCorpus name cs -runWorker FuzzWorker callback vm world dict workerId initialCorpus testLimit _ _ = runFuzzWorker callback vm world dict workerId initialCorpus testLimit +runWorker SymbolicWorker callback vm dict workerId initialCorpus _ name = + runSymWorker callback vm dict workerId initialCorpus name +runWorker FuzzWorker callback vm dict workerId initialCorpus testLimit _ = + runFuzzWorker callback vm dict workerId initialCorpus testLimit runSymWorker :: (MonadIO m, MonadThrow m, MonadReader Env m) @@ -109,9 +108,8 @@ runSymWorker -> [(FilePath, [Tx])] -- ^ Initial corpus of transactions -> Maybe Text -- ^ Specified contract name - -> [SolcContract] -- ^ List of contracts -> m (WorkerStopReason, WorkerState) -runSymWorker callback vm dict workerId initialCorpus name cs = do +runSymWorker callback vm dict workerId initialCorpus name = do cfg <- asks (.cfg) let nworkers = getNFuzzWorkers cfg.campaignConf -- getNFuzzWorkers, NOT getNWorkers eventQueue <- asks (.eventQueue) @@ -173,7 +171,9 @@ runSymWorker callback vm dict workerId initialCorpus name cs = do symexecTx (tx, vm', txsBase) = do cfg <- asks (.cfg) - (threadId, symTxsChan) <- liftIO $ createSymTx cfg name cs tx vm' + dapp <- asks (.dapp) + let compiledContracts = Map.elems dapp.solcByName + (threadId, symTxsChan) <- liftIO $ createSymTx cfg name compiledContracts tx vm' modify' (\ws -> ws { runningThreads = [threadId] }) lift callback @@ -196,14 +196,13 @@ runFuzzWorker => StateT WorkerState m () -- ^ Callback to run after each state update (for instrumentation) -> VM Concrete RealWorld -- ^ Initial VM state - -> World -- ^ Initial world state -> GenDict -- ^ Generation dictionary -> Int -- ^ Worker id starting from 0 -> [(FilePath, [Tx])] -- ^ Initial corpus of transactions -> Int -- ^ Test limit for this worker -> m (WorkerStopReason, WorkerState) -runFuzzWorker callback vm world dict workerId initialCorpus testLimit = do +runFuzzWorker callback vm dict workerId initialCorpus testLimit = do let effectiveSeed = dict.defSeed + workerId effectiveGenDict = dict { defSeed = effectiveSeed } @@ -226,55 +225,80 @@ runFuzzWorker callback vm world dict workerId initialCorpus testLimit = do where run = do - testsRef <- asks (.testsRef) - tests <- liftIO $ readIORef testsRef + testRefs <- asks (.testRefs) + tests <- liftIO $ traverse readIORef testRefs CampaignConf{stopOnFail, shrinkLimit} <- asks (.cfg.campaignConf) ncalls <- gets (.ncalls) let - final test = case test.state of - Solved -> True - Failed _ -> True - _ -> False - - shrinkable test = case test.state of - Large n -> n < shrinkLimit - _ -> False - - closeOptimizationTest test = case test.testType of - OptimizationTest _ _ -> test { Test.state = Large 0 } - _ -> test + shrinkable test = + case test.state of + -- we shrink only tests which were solved on this + -- worker, see 'updateOpenTest' + Large n | test.workerId == Just workerId -> + n < shrinkLimit + _ -> False + + final test = + case test.state of + Solved -> True + Failed _ -> True + _ -> False + + closeOptimizationTest test = + case test.testType of + OptimizationTest _ _ -> + test { Test.state = Large 0 + , workerId = Just workerId + } + _ -> test if | stopOnFail && any final tests -> lift callback >> pure FastFailed + -- we shrink first before going back to fuzzing + | any shrinkable tests -> + shrink >> lift callback >> run + + -- no shrinking work, fuzz | (null tests || any isOpen tests) && ncalls < testLimit -> - fuzz >> continue + fuzz >> lift callback >> run + -- NOTE: this is a hack which forces shrinking of optimization tests + -- after test limit is reached | ncalls >= testLimit && any (\t -> isOpen t && isOptimizationTest t) tests -> do - liftIO $ atomicModifyIORef' testsRef $ \sharedTests -> - (closeOptimizationTest <$> sharedTests, ()) - continue - - | any shrinkable tests -> - continue + liftIO $ forM_ testRefs $ \testRef -> + atomicModifyIORef' testRef (\test -> (closeOptimizationTest test, ())) + lift callback >> run + -- no more work to do, means we reached the test limit, exit | otherwise -> lift callback >> pure TestLimitReached - fuzz = randseq vm.env.contracts world >>= fmap fst . callseq vm - - continue = runUpdate (shrinkTest vm) >> lift callback >> run + fuzz = randseq vm.env.contracts >>= fmap fst . callseq vm + + -- To avoid contention we only shrink tests that were falsified by this + -- worker. Tests are marked with a worker in 'updateOpenTest'. + -- + -- TODO: This makes some workers run longer as they work less on their + -- test limit portion during shrinking. We should move to a test limit shared + -- between workers to avoid that. This way other workers will "drain" + -- the work queue. + shrink = updateTests $ \test -> do + if test.workerId == Just workerId then + shrinkTest vm test + else + pure Nothing -- | Generate a new sequences of transactions, either using the corpus or with -- randomly created transactions randseq :: (MonadRandom m, MonadReader Env m, MonadState WorkerState m, MonadIO m) => Map (Expr 'EAddr) Contract - -> World -> m [Tx] -randseq deployedContracts world = do +randseq deployedContracts = do env <- ask + let world = env.world let mutConsts = env.cfg.campaignConf.mutConsts @@ -437,8 +461,7 @@ updateGasInfo ((tx@Tx{call = SolCall (f, _)}, (_, used')):txs) tseq gi = updateGasInfo ((t, _):ts) tseq gi = updateGasInfo ts (t:tseq) gi -- | Given an initial 'VM' state and a way to run transactions, evaluate a list --- of transactions, constantly checking if we've solved any tests or can shrink --- known solves. +-- of transactions, constantly checking if we've solved any tests. evalSeq :: (MonadIO m, MonadThrow m, MonadRandom m, MonadReader Env m, MonadState WorkerState m) => VM Concrete RealWorld -- ^ Initial VM @@ -449,7 +472,7 @@ evalSeq vm0 execFunc = go vm0 [] where go vm executedSoFar toExecute = do -- NOTE: we do reverse here because we build up this list by prepending, -- see the last line of this function. - runUpdate (updateTest vm0 (vm, reverse executedSoFar)) + updateTests (updateOpenTest vm (reverse executedSoFar)) modify' $ \workerState -> workerState { ncalls = workerState.ncalls + 1 } case toExecute of [] -> pure ([], vm) @@ -462,54 +485,65 @@ evalSeq vm0 execFunc = go vm0 [] where (remaining, _vm) <- go vm' (tx:executedSoFar) remainingTxs pure ((tx, result) : remaining, vm') --- | Given a rule for updating a particular test's state, apply it to each test --- in a 'Campaign'. -runUpdate +-- | Update tests based on the return value from the given function. +-- Nothing skips the update. +updateTests :: (MonadIO m, MonadReader Env m, MonadState WorkerState m) => (EchidnaTest -> m (Maybe EchidnaTest)) -> m () -runUpdate f = do - testsRef <- asks (.testsRef) - tests <- liftIO $ readIORef testsRef - updates <- mapM f tests - when (any isJust updates) $ - liftIO $ atomicModifyIORef' testsRef $ \sharedTests -> - (uncurry fromMaybe <$> zip sharedTests updates, ()) - --- | Given an initial 'VM' state and a @('SolTest', 'TestState')@ pair, as well --- as possibly a sequence of transactions and the state after evaluation, see if: --- (0): The test is past its 'testLimit' or 'shrinkLimit' and should be presumed un[solve|shrink]able --- (1): The test is 'Open', and this sequence of transactions solves it --- (2): The test is 'Open', and evaluating it breaks our runtime --- (3): The test is unshrunk, and we can shrink it --- Then update accordingly, keeping track of how many times we've tried to solve or shrink. -updateTest +updateTests f = do + testRefs <- asks (.testRefs) + forM_ testRefs $ \testRef -> do + test <- liftIO $ readIORef testRef + f test >>= \case + Just test' -> liftIO $ writeIORef testRef test' + Nothing -> pure () + +-- | Update an open test after checking if it is falsified by the 'reproducer' +updateOpenTest :: (MonadIO m, MonadThrow m, MonadRandom m, MonadReader Env m, MonadState WorkerState m) - => VM Concrete RealWorld - -> (VM Concrete RealWorld, [Tx]) + => VM Concrete RealWorld -- ^ VM after applying potential reproducer + -> [Tx] -- ^ potential reproducer -> EchidnaTest -> m (Maybe EchidnaTest) -updateTest vmForShrink (vm, xs) test = do +updateOpenTest vm reproducer test = do case test.state of Open -> do (testValue, vm') <- checkETest test vm - let - results = getResultFromVM vm' - test' = updateOpenTest test xs (testValue, vm', results) - case test'.state of - Large _ -> do + let result = getResultFromVM vm' + case testValue of + BoolValue False -> do + workerId <- Just <$> gets (.workerId) + let test' = test { Test.state = Large 0 + , reproducer + , vm = Just vm + , result + , workerId + } pushWorkerEvent (TestFalsified test') - pure (Just test') - _ | test'.value > test.value -> do + pure $ Just test' + + IntValue value' | value' > value -> do + let test' = test { reproducer + , value = IntValue value' + , vm = Just vm + , result + } pushWorkerEvent (TestOptimized test') - pure (Just test') - _ -> pure Nothing - Large _ -> - -- TODO: We shrink already in `step`, but we shrink here too. It makes - -- shrink go faster when some tests are still fuzzed. It's not incorrect - -- but requires passing `vmForShrink` and feels a bit wrong. - shrinkTest vmForShrink test - _ -> pure Nothing + pure $ Just test' + where + value = + case test.value of + IntValue x -> x + -- TODO: fix this with proper types + _ -> error "Invalid type of value for optimization" + + _ -> + -- no luck with fuzzing this time + pure Nothing + _ -> + -- not an open test, skip + pure Nothing pushWorkerEvent :: (MonadReader Env m, MonadState WorkerState m, MonadIO m) diff --git a/lib/Echidna/Onchain.hs b/lib/Echidna/Onchain.hs index f791e5039..d26cfacd1 100644 --- a/lib/Echidna/Onchain.hs +++ b/lib/Echidna/Onchain.hs @@ -10,7 +10,7 @@ import Data.ByteString (ByteString) import Data.ByteString qualified as BS import Data.ByteString.UTF8 qualified as UTF8 import Data.Functor ((<&>)) -import Data.IORef (writeIORef, readIORef) +import Data.IORef (readIORef) import Data.Map (Map) import Data.Map qualified as Map import Data.Maybe (isJust, fromJust, fromMaybe) @@ -100,10 +100,14 @@ toFetchedContractData contract = -- | Try to load the persisted RPC cache. -- TODO: we use the corpus dir for now, think where to place it -loadRpcCache :: Env -> IO () -loadRpcCache Env { cfg, fetchContractCache, fetchSlotCache } = +loadRpcCache + :: EConfig + -> IO ( Map Addr (Maybe Contract) + , Map Addr (Map W256 (Maybe W256)) + ) +loadRpcCache cfg = case cfg.campaignConf.corpusDir of - Nothing -> pure () + Nothing -> pure (mempty, mempty) Just dir -> do let cache_dir = dir "cache" createDirectoryIfMissing True cache_dir @@ -115,10 +119,12 @@ loadRpcCache Env { cfg, fetchContractCache, fetchSlotCache } = parsedSlots :: Maybe (Map Addr (Map W256 (Maybe W256))) <- readFileIfExists (cache_dir "block_" <> show block <> "_fetch_cache_slots.json") <&> (>>= JSON.decodeStrict) - writeIORef fetchContractCache (maybe mempty (Map.map (Just . fromFetchedContractData)) parsedContracts) - writeIORef fetchSlotCache (fromMaybe mempty parsedSlots) + pure + ( maybe mempty (Map.map (Just . fromFetchedContractData)) parsedContracts + , fromMaybe mempty parsedSlots + ) Nothing -> - pure () + pure (mempty, mempty) readFileIfExists :: FilePath -> IO (Maybe BS.ByteString) readFileIfExists path = do diff --git a/lib/Echidna/Output/JSON.hs b/lib/Echidna/Output/JSON.hs index ebacc8183..39bcaa503 100644 --- a/lib/Echidna/Output/JSON.hs +++ b/lib/Echidna/Output/JSON.hs @@ -100,7 +100,7 @@ instance ToJSON Transaction where encodeCampaign :: Env -> [WorkerState] -> IO L.ByteString encodeCampaign env workerStates = do - tests <- readIORef env.testsRef + tests <- traverse readIORef env.testRefs frozenCov <- mapM VU.freeze =<< readIORef env.coverageRef -- TODO: this is ugly, refactor seed to live in Env let worker0 = Prelude.head workerStates diff --git a/lib/Echidna/Solidity.hs b/lib/Echidna/Solidity.hs index 1868ff246..b7e599e16 100644 --- a/lib/Echidna/Solidity.hs +++ b/lib/Echidna/Solidity.hs @@ -30,7 +30,6 @@ import System.Info (os) import EVM (initialContract, currentContract) import EVM.ABI -import EVM.Dapp (DappInfo(..)) import EVM.Solidity import EVM.Types hiding (Env) @@ -42,7 +41,6 @@ import Echidna.Etheno (loadEthenoBatch) import Echidna.Events (extractEvents) import Echidna.Exec (execTx, initialVM) import Echidna.SourceAnalysis.Slither -import Echidna.Symbolic (forceAddr) import Echidna.Test (createTests, isAssertionMode, isPropertyMode, isDapptestMode) import Echidna.Types.Config (EConfig(..), Env(..)) import Echidna.Types.Signature @@ -167,48 +165,12 @@ abiOf pref solcContract = -- filename their code is in, plus a colon. loadSpecified :: Env - -> Maybe Text + -> SolcContract -> [SolcContract] - -> IO (VM Concrete RealWorld, [SolSignature], [Text], SignatureMap) -loadSpecified env name cs = do + -> IO (VM Concrete RealWorld) +loadSpecified env mainContract cs = do let solConf = env.cfg.solConf - -- Pick contract to load - mainContract <- chooseContract cs name - when (isNothing name && length cs > 1 && not solConf.quiet) $ - putStrLn "Multiple contracts found, only analyzing the first" - unless solConf.quiet $ - putStrLn $ "Analyzing contract: " <> T.unpack mainContract.contractName - - let - -- generate the complete abi mapping - abi = Map.elems mainContract.abiMap <&> \method -> (method.name, snd <$> method.inputs) - (tests, funs) = partition (isPrefixOf solConf.prefix . fst) abi - - -- Filter ABI according to the config options - fabiOfc = if isDapptestMode solConf.testMode - then NE.toList $ filterMethodsWithArgs (abiOf solConf.prefix mainContract) - else filterMethods mainContract.contractName solConf.methodFilter $ - abiOf solConf.prefix mainContract - -- Filter again for dapptest tests or assertions checking if enabled - neFuns = filterMethods mainContract.contractName solConf.methodFilter (fallback NE.:| funs) - -- Construct ABI mapping for World - abiMapping = - if solConf.allContracts then - Map.fromList $ mapMaybe (\contract -> - let filtered = filterMethods contract.contractName - solConf.methodFilter - (abiOf solConf.prefix contract) - in (contract.runtimeCodehash,) <$> NE.nonEmpty filtered) - cs - else - case NE.nonEmpty fabiOfc of - Just ne -> Map.singleton mainContract.runtimeCodehash ne - Nothing -> mempty - - when (Map.null abiMapping) $ - throwM $ InvalidMethodFilters solConf.methodFilter - -- Set up initial VM, either with chosen contract or Etheno initialization file -- need to use snd to add to ABI dict initVM <- stToIO $ initialVM solConf.allowFFI @@ -225,20 +187,6 @@ loadSpecified env name cs = do -- Select libraries ls <- mapM (chooseContract cs . Just . T.pack) solConf.solcLibs - -- Make sure everything is ready to use, then ship it - when (null abi) $ - throwM NoFuncs - when (null tests && isPropertyMode solConf.testMode) $ - throwM NoTests - when (null abiMapping && isDapptestMode solConf.testMode) $ - throwM NoTests - when (mainContract.creationCode == mempty) $ - throwM (NoBytecode mainContract.contractName) - - case find (not . null . snd) tests of - Just (t, _) -> throwM $ TestArgsFound t - Nothing -> pure () - flip runReaderT env $ do -- library deployment vm0 <- deployContracts (zip [addrLibrary ..] ls) solConf.deployer blank @@ -262,24 +210,104 @@ loadSpecified env name cs = do when (isNothing $ currentContract vm3) $ throwM $ DeploymentFailed solConf.contractAddr $ T.unlines $ extractEvents True env.dapp vm3 - -- Run - let transaction = execTx vm3 $ uncurry basicTx - setUpFunction - solConf.deployer - solConf.contractAddr - unlimitedGasPerBlock - (0, 0) + -- Run setUp function + let + abi = Map.elems mainContract.abiMap <&> \method -> (method.name, snd <$> method.inputs) + transaction = execTx vm3 $ uncurry basicTx + setUpFunction + solConf.deployer + solConf.contractAddr + unlimitedGasPerBlock + (0, 0) vm4 <- if isDapptestMode solConf.testMode && setUpFunction `elem` abi then snd <$> transaction else pure vm3 case vm4.result of Just (VMFailure _) -> throwM SetUpCallFailed - _ -> pure (vm4, neFuns, fst <$> tests, abiMapping) + _ -> pure vm4 where setUpFunction = ("setUp", []) + +selectMainContract + :: SolConf + -> Maybe ContractName + -> [SolcContract] + -> IO SolcContract +selectMainContract solConf name cs = do + -- Pick contract to load + mainContract <- chooseContract cs name + when (isNothing name && length cs > 1 && not solConf.quiet) $ + putStrLn "Multiple contracts found, only analyzing the first" + unless solConf.quiet $ + putStrLn $ "Analyzing contract: " <> T.unpack mainContract.contractName + when (mainContract.creationCode == mempty) $ + throwM (NoBytecode mainContract.contractName) + pure mainContract + +mkSignatureMap + :: SolConf + -> SolcContract + -> [SolcContract] + -> IO SignatureMap +mkSignatureMap solConf mainContract contracts = do + let + -- Filter ABI according to the config options + fabiOfc = if isDapptestMode solConf.testMode + then NE.toList $ filterMethodsWithArgs (abiOf solConf.prefix mainContract) + else filterMethods mainContract.contractName solConf.methodFilter $ + abiOf solConf.prefix mainContract + -- Construct ABI mapping for World + abiMapping = + if solConf.allContracts then + Map.fromList $ mapMaybe (\contract -> + let filtered = filterMethods contract.contractName + solConf.methodFilter + (abiOf solConf.prefix contract) + in (contract.runtimeCodehash,) <$> NE.nonEmpty filtered) + contracts + else + case NE.nonEmpty fabiOfc of + Just ne -> Map.singleton mainContract.runtimeCodehash ne + Nothing -> mempty + when (null abiMapping && isDapptestMode solConf.testMode) $ + throwM NoTests + when (Map.null abiMapping) $ + throwM $ InvalidMethodFilters solConf.methodFilter + pure abiMapping + +mkTests + :: SolConf + -> SolcContract + -> IO [EchidnaTest] +mkTests solConf mainContract = do + let + -- generate the complete abi mapping + abi = Map.elems mainContract.abiMap <&> \method -> (method.name, snd <$> method.inputs) + (tests, funs) = partition (isPrefixOf solConf.prefix . fst) abi + -- Filter again for dapptest tests or assertions checking if enabled + neFuns = filterMethods mainContract.contractName + solConf.methodFilter + (fallback NE.:| funs) + testNames = fst <$> tests + + when (null abi) $ + throwM NoFuncs + when (null tests && isPropertyMode solConf.testMode) $ + throwM NoTests + + case find (not . null . snd) tests of + Just (t, _) -> throwM $ TestArgsFound t + Nothing -> pure () + + pure $ createTests solConf.testMode + solConf.testDestruction + testNames + solConf.contractAddr + neFuns + -- | Given a list of contracts and a requested contract name, pick a contract. -- See 'loadSpecified' for more information. chooseContract :: (MonadThrow m) => [SolcContract] -> Maybe Text -> m SolcContract @@ -369,25 +397,6 @@ prepareHashMaps cs as m = filterHashMap f xs = Map.mapMaybe (NE.nonEmpty . NE.filter (\s -> f $ (hashSig . encodeSig $ s) `elem` xs)) --- | Given a file and an optional contract name, compile the file as solidity, then, if a name is --- given, try to fine the specified contract (assuming it is in the file provided), otherwise, find --- the first contract in the file. Take said contract and return an initial VM state with it loaded, --- its ABI (as 'SolSignature's), and the names of its Echidna tests. NOTE: unlike 'loadSpecified', --- contract names passed here don't need the file they occur in specified. -loadSolTests - :: Env - -> Maybe Text - -> IO (VM Concrete RealWorld, World, [EchidnaTest]) -loadSolTests env name = do - let solConf = env.cfg.solConf - let contracts = Map.elems env.dapp.solcByName - (vm, funs, testNames, _signatureMap) <- loadSpecified env name contracts - let - eventMap = Map.unions $ map (.eventMap) contracts - world = World solConf.sender mempty Nothing [] eventMap - echidnaTests = createTests solConf.testMode True testNames (forceAddr vm.state.contract) funs - pure (vm, world, echidnaTests) - mkLargeAbiInt :: Int -> AbiValue mkLargeAbiInt i = AbiInt i $ 2 ^ (i - 1) - 1 diff --git a/lib/Echidna/Test.hs b/lib/Echidna/Test.hs index 05837fe41..e11886f3d 100644 --- a/lib/Echidna/Test.hs +++ b/lib/Echidna/Test.hs @@ -24,7 +24,6 @@ import Echidna.Symbolic (forceBuf) import Echidna.Types.Config import Echidna.Types.Signature (SolSignature) import Echidna.Types.Test -import Echidna.Types.Test qualified as Test import Echidna.Types.Tx (Tx, TxConf(..), basicTx, TxResult(..), getResult) --- | Possible responses to a call to an Echidna test: @true@, @false@, @REVERT@, and ???. @@ -47,7 +46,7 @@ getResultFromVM vm = Nothing -> error "getResultFromVM failed" createTest :: TestType -> EchidnaTest -createTest m = EchidnaTest Open m v [] Stop Nothing +createTest m = EchidnaTest Open m v [] Stop Nothing Nothing where v = case m of PropertyTest _ _ -> BoolValue True OptimizationTest _ _ -> IntValue minBound @@ -111,6 +110,7 @@ createTests m td ts r ss = case m of sdt = createTest (CallTest "Target contract is not self-destructed" $ checkSelfDestructedTarget r) sdat = createTest (CallTest "No contract can be self-destructed" checkAnySelfDestructed) + {- updateOpenTest :: EchidnaTest -> [Tx] @@ -133,6 +133,7 @@ updateOpenTest test txs (IntValue v', vm, r) = IntValue x -> x _ -> error "Invalid type of value for optimization" updateOpenTest _ _ _ = error "Invalid type of test" +-} -- | Given a 'SolTest', evaluate it and see if it currently passes. checkETest diff --git a/lib/Echidna/Types/Config.hs b/lib/Echidna/Types/Config.hs index 64bae8b79..62f4e7513 100644 --- a/lib/Echidna/Types/Config.hs +++ b/lib/Echidna/Types/Config.hs @@ -19,6 +19,7 @@ import Echidna.Types.Coverage (CoverageMap) import Echidna.Types.Solidity (SolConf) import Echidna.Types.Test (TestConf, EchidnaTest) import Echidna.Types.Tx (TxConf) +import Echidna.Types.World (World) data OperationMode = Interactive | NonInteractive OutputFormat deriving (Show, Eq) data OutputFormat = Text | JSON | None deriving (Show, Eq) @@ -68,7 +69,7 @@ data Env = Env -- minimal. , eventQueue :: Chan (LocalTime, CampaignEvent) - , testsRef :: IORef [EchidnaTest] + , testRefs :: [IORef EchidnaTest] , coverageRef :: IORef CoverageMap , corpusRef :: IORef Corpus @@ -76,4 +77,5 @@ data Env = Env , fetchContractCache :: IORef (Map Addr (Maybe Contract)) , fetchSlotCache :: IORef (Map Addr (Map W256 (Maybe W256))) , chainId :: Maybe W256 + , world :: World } diff --git a/lib/Echidna/Types/Test.hs b/lib/Echidna/Types/Test.hs index 2221aa39e..7f6e00925 100644 --- a/lib/Echidna/Types/Test.hs +++ b/lib/Echidna/Types/Test.hs @@ -102,6 +102,8 @@ data EchidnaTest = EchidnaTest , reproducer :: [Tx] , result :: TxResult , vm :: Maybe (VM Concrete RealWorld) + -- | Worker which falsified the test will also shrink it. + , workerId :: Maybe Int } deriving (Show) instance ToJSON EchidnaTest where diff --git a/lib/Echidna/UI.hs b/lib/Echidna/UI.hs index 8e12e90e4..5387aed51 100644 --- a/lib/Echidna/UI.hs +++ b/lib/Echidna/UI.hs @@ -61,13 +61,11 @@ data UIEvent = ui :: (MonadCatch m, MonadReader Env m, MonadUnliftIO m) => VM Concrete RealWorld -- ^ Initial VM state - -> World -- ^ Initial world state -> GenDict -> [(FilePath, [Tx])] -> Maybe Text - -> [SolcContract] -> m [WorkerState] -ui vm world dict initialCorpus cliSelectedContract cs = do +ui vm dict initialCorpus cliSelectedContract = do env <- ask conf <- asks (.cfg) terminalPresent <- liftIO isTerminal @@ -106,7 +104,7 @@ ui vm world dict initialCorpus cliSelectedContract cs = do threadDelay 200_000 -- 200 ms now <- getTimestamp - tests <- readIORef env.testsRef + tests <- traverse readIORef env.testRefs states <- workerStates workers writeBChan uiChannel (CampaignUpdated now tests states) @@ -124,7 +122,7 @@ ui vm world dict initialCorpus cliSelectedContract cs = do app <- customMain initialVty buildVty (Just uiChannel) <$> monitor liftIO $ do - tests <- readIORef env.testsRef + tests <- traverse readIORef env.testRefs now <- getTimestamp void $ app UIState { campaigns = [initialWorkerState] -- ugly, fix me @@ -229,7 +227,7 @@ ui vm world dict initialCorpus cliSelectedContract cs = do corpus = if workerType == SymbolicWorker then initialCorpus else corpusChunk maybeResult <- timeout timeoutUsecs $ runWorker workerType (get >>= writeIORef stateRef) - vm world dict workerId corpus testLimit cliSelectedContract cs + vm dict workerId corpus testLimit cliSelectedContract pure $ case maybeResult of Just (stopReason, _finalState) -> stopReason Nothing -> TimeLimitReached @@ -355,7 +353,7 @@ statusLine -> [WorkerState] -> IO String statusLine env states = do - tests <- readIORef env.testsRef + tests <- traverse readIORef env.testRefs points <- scoveragePoints =<< readIORef env.coverageRef corpus <- readIORef env.corpusRef let totalCalls = sum ((.ncalls) <$> states) diff --git a/lib/Echidna/UI/Report.hs b/lib/Echidna/UI/Report.hs index fa6d47b3e..23ee7bb2f 100644 --- a/lib/Echidna/UI/Report.hs +++ b/lib/Echidna/UI/Report.hs @@ -45,7 +45,7 @@ ppCampaignEventLog vm ev = (ppCampaignEvent ev <>) <$> ppTxIfHas where ppCampaign :: (MonadIO m, MonadReader Env m) => VM Concrete RealWorld -> [WorkerState] -> m String ppCampaign vm workerStates = do - tests <- liftIO . readIORef =<< asks (.testsRef) + tests <- liftIO . traverse readIORef =<< asks (.testRefs) testsPrinted <- ppTests tests gasInfoPrinted <- ppGasInfo vm workerStates coveragePrinted <- ppCoverage diff --git a/lib/Echidna/UI/Widgets.hs b/lib/Echidna/UI/Widgets.hs index 6426ef848..a343eb0b9 100644 --- a/lib/Echidna/UI/Widgets.hs +++ b/lib/Echidna/UI/Widgets.hs @@ -31,7 +31,7 @@ import Echidna.ABI import Echidna.Types.Campaign import Echidna.Types.Config import Echidna.Types.Test -import Echidna.Types.Tx (Tx(..), TxResult(..)) +import Echidna.Types.Tx (Tx(..)) import Echidna.UI.Report import Echidna.Utility (timePrefix) @@ -304,13 +304,13 @@ tsWidget => TestState -> EchidnaTest -> m (Widget Name, Widget Name) -tsWidget (Failed e) _ = pure (str "could not evaluate", str $ show e) -tsWidget Solved t = failWidget Nothing t.reproducer (fromJust t.vm) t.value t.result -tsWidget Passed _ = pure (success $ str "PASSED!", emptyWidget) -tsWidget Open _ = pure (success $ str "passing", emptyWidget) -tsWidget (Large n) t = do +tsWidget (Failed e) _ = pure (str "could not evaluate", str $ show e) +tsWidget Solved test = failWidget Nothing test +tsWidget Passed _ = pure (success $ str "PASSED!", emptyWidget) +tsWidget Open _ = pure (success $ str "passing", emptyWidget) +tsWidget (Large n) test = do m <- asks (.cfg.campaignConf.shrinkLimit) - failWidget (if n < m then Just (n,m) else Nothing) t.reproducer (fromJust t.vm) t.value t.result + failWidget (if n < m then Just (n,m) else Nothing) test titleWidget :: Widget n titleWidget = str "Call sequence" <+> str ":" @@ -329,25 +329,21 @@ tracesWidget vm = do failWidget :: MonadReader Env m => Maybe (Int, Int) - -> [Tx] - -> VM Concrete RealWorld - -> TestValue - -> TxResult + -> EchidnaTest -> m (Widget Name, Widget Name) -failWidget _ [] _ _ _= pure (failureBadge, str "*no transactions made*") -failWidget b xs vm _ r = do - s <- seqWidget vm xs +failWidget _ test | null test.reproducer = + pure (failureBadge, str "*no transactions made*") +failWidget b test = do + -- TODO: we know this is set for failed tests, ideally we should improve this + -- with better types in EchidnaTest + let vm = fromJust test.vm + s <- seqWidget vm test.reproducer traces <- tracesWidget vm pure - ( failureBadge <+> str (" with " ++ show r) - , status <=> titleWidget <=> s <=> str " " <=> traces + ( failureBadge <+> str (" with " ++ show test.result) + , shrinkWidget b test <=> titleWidget <=> s <=> str " " <=> traces ) where - status = case b of - Nothing -> emptyWidget - Just (n,m) -> - str "Current action: " <+> - withAttr (attrName "working") (str ("shrinking " ++ progress n m)) optWidget :: MonadReader Env m @@ -360,31 +356,36 @@ optWidget Passed t = pure (str $ "max value found: " ++ show t.value, emptyW optWidget Open t = pure (withAttr (attrName "working") $ str $ "optimizing, max value: " ++ show t.value, emptyWidget) -optWidget (Large n) t = do +optWidget (Large n) test = do m <- asks (.cfg.campaignConf.shrinkLimit) - maxWidget (if n < m then Just (n,m) else Nothing) t.reproducer (fromJust t.vm) t.value + maxWidget (if n < m then Just (n,m) else Nothing) test maxWidget :: MonadReader Env m => Maybe (Int, Int) - -> [Tx] - -> VM Concrete RealWorld - -> TestValue + -> EchidnaTest -> m (Widget Name, Widget Name) -maxWidget _ [] _ _ = pure (failureBadge, str "*no transactions made*") -maxWidget b xs vm v = do - s <- seqWidget vm xs +maxWidget _ test | null test.reproducer = + pure (failureBadge, str "*no transactions made*") +maxWidget b test = do + let vm = fromJust test.vm + s <- seqWidget vm test.reproducer traces <- tracesWidget vm pure - ( maximumBadge <+> str (" max value: " ++ show v) - , status <=> titleWidget <=> s <=> str " " <=> traces + ( maximumBadge <+> str (" max value: " ++ show test.value) + , shrinkWidget b test <=> titleWidget <=> s <=> str " " <=> traces ) - where - status = case b of + +shrinkWidget :: Maybe (Int, Int) -> EchidnaTest -> Widget Name +shrinkWidget b test = + case b of Nothing -> emptyWidget Just (n,m) -> str "Current action: " <+> - withAttr (attrName "working") (str ("shrinking " ++ progress n m)) + withAttr (attrName "working") + (str ("shrinking " ++ progress n m ++ showWorker)) + where + showWorker = maybe "" (\i -> " (worker " <> show i <> ")") test.workerId seqWidget :: MonadReader Env m => VM Concrete RealWorld -> [Tx] -> m (Widget Name) seqWidget vm xs = do diff --git a/src/Main.hs b/src/Main.hs index d64f4023a..9e1d68d49 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -29,7 +29,7 @@ import System.IO (hPutStrLn, stderr) import System.IO.CodePage (withCP65001) import EVM.Dapp (DappInfo(..)) -import EVM.Solidity (BuildOutput(..), Contracts(..)) +import EVM.Solidity (BuildOutput(..)) import EVM.Types (Addr) import Echidna @@ -59,20 +59,16 @@ main = withUtf8 $ withCP65001 $ do forM_ ks $ hPutStrLn stderr . ("Warning: unused option: " ++) . Aeson.Key.toString buildOutput <- compileContracts cfg.solConf cliFilePath - env <- mkEnv cfg buildOutput - - Onchain.loadRpcCache env -- take the seed from config, otherwise generate a new one seed <- maybe (getRandomR (0, maxBound)) pure cfg.campaignConf.seed - (vm, world, dict) <- prepareContract env cliFilePath cliSelectedContract seed + (vm, env, dict) <- prepareContract cfg cliFilePath buildOutput cliSelectedContract seed - initialCorpus <- loadInitialCorpus env world - let (Contracts contractMap) = buildOutput.contracts + initialCorpus <- loadInitialCorpus env -- start ui and run tests - _campaign <- runReaderT (ui vm world dict initialCorpus cliSelectedContract (Map.elems contractMap)) env + _campaign <- runReaderT (ui vm dict initialCorpus cliSelectedContract) env - tests <- readIORef env.testsRef + tests <- traverse readIORef env.testRefs Onchain.saveRpcCache env diff --git a/src/test/Common.hs b/src/test/Common.hs index ae658a76a..884e11384 100644 --- a/src/test/Common.hs +++ b/src/test/Common.hs @@ -18,6 +18,7 @@ module Common , gasInRange , countCorpus , overrideQuiet + , loadSolTests ) where import Test.Tasty (TestTree) @@ -26,6 +27,7 @@ import Test.Tasty.HUnit (testCase, assertBool) import Control.Monad (forM_) import Control.Monad.Reader (runReaderT) import Control.Monad.Random (getRandomR) +import Control.Monad.ST (RealWorld) import Data.DoubleWord (Int256) import Data.Function ((&)) import Data.IORef @@ -40,7 +42,7 @@ import System.Process (readProcess) import Echidna (mkEnv, prepareContract) import Echidna.Config (parseConfig, defaultConfig) import Echidna.Campaign (runWorker) -import Echidna.Solidity (loadSolTests, compileContracts) +import Echidna.Solidity (selectMainContract, mkTests, loadSpecified, compileContracts) import Echidna.Test (checkETest) import Echidna.Types (Gas) import Echidna.Types.Config (Env(..), EConfig(..), EConfigWithUsage(..)) @@ -49,8 +51,10 @@ import Echidna.Types.Signature (ContractName) import Echidna.Types.Solidity (SolConf(..)) import Echidna.Types.Test import Echidna.Types.Tx (Tx(..), TxCall(..), call) +import Echidna.Types.World (World(..)) -import EVM.Solidity (Contracts(..), BuildOutput(..)) +import EVM.Solidity (Contracts(..), BuildOutput(..), SolcContract(..)) +import EVM.Types hiding (Env, Gas) testConfig :: EConfig testConfig = defaultConfig & overrideQuiet @@ -89,14 +93,11 @@ runContract :: FilePath -> Maybe ContractName -> EConfig -> WorkerType -> IO (En runContract f selectedContract cfg workerType = do seed <- maybe (getRandomR (0, maxBound)) pure cfg.campaignConf.seed buildOutput <- compileContracts cfg.solConf (f :| []) - env <- mkEnv cfg buildOutput - (vm, world, dict) <- prepareContract env (f :| []) selectedContract seed - - let (Contracts contractMap) = buildOutput.contracts + (vm, env, dict) <- prepareContract cfg (f :| []) buildOutput selectedContract seed (_stopReason, finalState) <- flip runReaderT env $ - runWorker workerType (pure ()) vm world dict 0 [] cfg.campaignConf.testLimit selectedContract (Map.elems contractMap) + runWorker workerType (pure ()) vm dict 0 [] cfg.campaignConf.testLimit selectedContract -- TODO: consider snapshotting the state so checking function don't need to -- be IO @@ -138,12 +139,33 @@ testContract' fp n v configPath s workerType expectations = testCase fp $ withSo forM_ expectations $ \(message, assertion) -> do assertion result >>= assertBool message +-- | Given a file and an optional contract name, compile the file as solidity, then, if a name is +-- given, try to fine the specified contract (assuming it is in the file provided), otherwise, find +-- the first contract in the file. Take said contract and return an initial VM state with it loaded, +-- its ABI (as 'SolSignature's), and the names of its Echidna tests. NOTE: unlike 'loadSpecified', +-- contract names passed here don't need the file they occur in specified. +loadSolTests + :: EConfig + -> BuildOutput + -> Maybe Text + -> IO (VM Concrete RealWorld, Env, [EchidnaTest]) +loadSolTests cfg buildOutput name = do + let solConf = cfg.solConf + (Contracts contractMap) = buildOutput.contracts + contracts = Map.elems contractMap + eventMap = Map.unions $ map (.eventMap) contracts + world = World solConf.sender mempty Nothing [] eventMap + mainContract <- selectMainContract solConf name contracts + echidnaTests <- mkTests solConf mainContract + env <- mkEnv cfg buildOutput echidnaTests world + vm <- loadSpecified env mainContract contracts + pure (vm, env, echidnaTests) + checkConstructorConditions :: FilePath -> String -> TestTree checkConstructorConditions fp as = testCase fp $ do let cfg = testConfig buildOutput <- compileContracts cfg.solConf (pure fp) - env <- mkEnv cfg buildOutput - (v, _, t) <- loadSolTests env Nothing + (v, env, t) <- loadSolTests cfg buildOutput Nothing r <- flip runReaderT env $ mapM (`checkETest` v) t mapM_ (\(x,_) -> assertBool as (forceBool x)) r where forceBool (BoolValue b) = b @@ -165,7 +187,7 @@ getResult n tests = optnFor :: Text -> (Env, WorkerState) -> IO (Maybe TestValue) optnFor n (env, _) = do - tests <- readIORef env.testsRef + tests <- traverse readIORef env.testRefs pure $ case getResult n tests of Just t -> Just t.value _ -> Nothing @@ -180,7 +202,7 @@ optimized n v final = do solnFor :: Text -> (Env, WorkerState) -> IO (Maybe [Tx]) solnFor n (env, _) = do - tests <- readIORef env.testsRef + tests <- traverse readIORef env.testRefs pure $ case getResult n tests of Just t -> if null t.reproducer then Nothing else Just t.reproducer _ -> Nothing @@ -190,7 +212,7 @@ solved t f = isJust <$> solnFor t f passed :: Text -> (Env, WorkerState) -> IO Bool passed n (env, _) = do - tests <- readIORef env.testsRef + tests <- traverse readIORef env.testRefs pure $ case getResult n tests of Just t | isPassed t -> True Just t | isOpen t -> True diff --git a/src/test/Tests/Compile.hs b/src/test/Tests/Compile.hs index c4048e504..0b30583ec 100644 --- a/src/test/Tests/Compile.hs +++ b/src/test/Tests/Compile.hs @@ -3,13 +3,12 @@ module Tests.Compile (compilationTests) where import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (testCase, assertBool) -import Common (testConfig) +import Common (testConfig, loadSolTests) import Control.Monad (void) import Control.Monad.Catch (catch) import Data.Text (Text) -import Echidna (mkEnv) -import Echidna.Solidity (compileContracts, loadSolTests) +import Echidna.Solidity (compileContracts) import Echidna.Types.Solidity (SolException(..)) import Echidna.Types.Config (EConfig(..)) @@ -42,5 +41,4 @@ loadFails fp c e p = testCase fp . catch tryLoad $ assertBool e . p where tryLoad = do let cfg = testConfig buildOutput <- compileContracts cfg.solConf (pure fp) - env <- mkEnv cfg buildOutput - void $ loadSolTests env c + void $ loadSolTests cfg buildOutput c diff --git a/src/test/Tests/Seed.hs b/src/test/Tests/Seed.hs index 78897b921..7793af1c3 100644 --- a/src/test/Tests/Seed.hs +++ b/src/test/Tests/Seed.hs @@ -40,5 +40,5 @@ seedTests = & overrideQuiet gen s = do (env, _) <- runContract "basic/flags.sol" Nothing (cfg s) FuzzWorker - readIORef env.testsRef + traverse readIORef env.testRefs same s t = (\x y -> ((.reproducer) <$> x) == ((.reproducer) <$> y)) <$> gen s <*> gen t From 4b9df9d1f9a72572a9ca2a27093da7424d45b8f0 Mon Sep 17 00:00:00 2001 From: samalws-tob <129795909+samalws-tob@users.noreply.github.com> Date: Fri, 5 Jul 2024 15:41:40 -0400 Subject: [PATCH 18/29] minor change for symExecTimeout comments (#1285) --- lib/Echidna/Types/Campaign.hs | 2 +- tests/solidity/basic/default.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Echidna/Types/Campaign.hs b/lib/Echidna/Types/Campaign.hs index 158b69f74..78a885189 100644 --- a/lib/Echidna/Types/Campaign.hs +++ b/lib/Echidna/Types/Campaign.hs @@ -51,7 +51,7 @@ data CampaignConf = CampaignConf -- Only relevant if symExec is True , symExecTargets :: Maybe [Text] , symExecTimeout :: Int - -- ^ Timeout for symbolic execution SMT solver. + -- ^ Timeout for symbolic execution SMT solver queries. -- Only relevant if symExec is True , symExecNSolvers :: Int -- ^ Number of SMT solvers used in symbolic execution. diff --git a/tests/solidity/basic/default.yaml b/tests/solidity/basic/default.yaml index 36428ac49..d9c277287 100644 --- a/tests/solidity/basic/default.yaml +++ b/tests/solidity/basic/default.yaml @@ -101,7 +101,7 @@ symExecConcolic: true # number of SMT solvers used in symbolic execution # only relevant if symExec is true symExecNSolvers: 1 -# timeout for symbolic execution SMT solver +# timeout for symbolic execution SMT solver queries # only relevant if symExec is true symExecTimeout: 30 # Number of times we may revisit a particular branching point From bf14ea4f32e69c9cd718cafbf4a51ebad6de7ca1 Mon Sep 17 00:00:00 2001 From: samalws-tob <129795909+samalws-tob@users.noreply.github.com> Date: Mon, 8 Jul 2024 09:36:55 -0400 Subject: [PATCH 19/29] show trace on UnknownFailure (#1283) --- lib/Echidna/Etheno.hs | 2 +- lib/Echidna/Exec.hs | 17 ++++++++++++----- lib/Echidna/Types.hs | 11 +++++++---- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/lib/Echidna/Etheno.hs b/lib/Echidna/Etheno.hs index 0c4f1008b..702e4ff87 100644 --- a/lib/Echidna/Etheno.hs +++ b/lib/Echidna/Etheno.hs @@ -178,7 +178,7 @@ execEthenoTxs et = do (_ , AccountCreated _) -> pure () (Reversion, _) -> void $ put vm (HandleEffect (Query q), _) -> crashWithQueryError q et - (VMFailure x, _) -> vmExcept x >> M.fail "impossible" + (VMFailure x, _) -> vmExcept Nothing x >> M.fail "impossible" (VMSuccess (ConcreteBuf bc), ContractCreated _ ca _ _ _ _) -> do #env % #contracts % at (LitAddr ca) % _Just % #code .= InitCode mempty mempty diff --git a/lib/Echidna/Exec.hs b/lib/Echidna/Exec.hs index afedf6245..e6711fe13 100644 --- a/lib/Echidna/Exec.hs +++ b/lib/Echidna/Exec.hs @@ -24,9 +24,10 @@ import System.Process (readProcessWithExitCode) import EVM (bytecode, replaceCodeOfSelf, loadContract, exec1, vmOpIx) import EVM.ABI +import EVM.Dapp (DappInfo) import EVM.Exec (exec, vmForEthrunCreation) import EVM.Fetch qualified -import EVM.Format (hexText) +import EVM.Format (hexText, showTraceTree) import EVM.Types hiding (Env, Gas) import Echidna.Events (emptyEvents) @@ -70,9 +71,12 @@ pattern Illegal :: VMResult Concrete s pattern Illegal <- VMFailure (classifyError -> IllegalE) -- | Given an execution error, throw the appropriate exception. -vmExcept :: MonadThrow m => EvmError -> m () -vmExcept e = throwM $ - case VMFailure e of {Illegal -> IllegalExec e; _ -> UnknownFailure e} +-- Also optionally takes a DappInfo and VM, which are used to show the stack trace. +vmExcept :: MonadThrow m => Maybe (DappInfo, VM Concrete RealWorld) -> EvmError -> m () +vmExcept traceInfo e = + let trace = uncurry showTraceTree <$> traceInfo + in throwM $ + case VMFailure e of {Illegal -> IllegalExec e; _ -> UnknownFailure e trace} execTxWith :: (MonadIO m, MonadState (VM Concrete RealWorld) m, MonadReader Env m, MonadThrow m) @@ -201,7 +205,10 @@ execTxWith executeTx tx = do #state % #callvalue .= callvalueBeforeVMReset #traces .= tracesBeforeVMReset #state % #codeContract .= codeContractBeforeVMReset - (VMFailure x, _) -> vmExcept x + (VMFailure x, _) -> do + dapp <- asks (.dapp) + vm <- get + vmExcept (Just (dapp, vm)) x (VMSuccess (ConcreteBuf bytecode'), SolCreate _) -> do -- Handle contract creation. #env % #contracts % at (LitAddr tx.dst) % _Just % #code .= InitCode mempty mempty diff --git a/lib/Echidna/Types.hs b/lib/Echidna/Types.hs index 316d49e83..f21232d59 100644 --- a/lib/Echidna/Types.hs +++ b/lib/Echidna/Types.hs @@ -3,22 +3,25 @@ module Echidna.Types where import Control.Exception (Exception) import Control.Monad.State.Strict (MonadState, get, put, MonadIO(liftIO), runStateT) import Control.Monad.ST (RealWorld, stToIO) +import Data.Text (Text, unpack) import Data.Word (Word64) import EVM (initialContract) import EVM.Types -- | We throw this when our execution fails due to something other than reversion. -data ExecException = IllegalExec EvmError | UnknownFailure EvmError +-- The `Maybe Text` on `UnknownFailure` is an optional stack trace. +data ExecException = IllegalExec EvmError | UnknownFailure EvmError (Maybe Text) instance Show ExecException where show = \case IllegalExec e -> "VM attempted an illegal operation: " ++ show e - UnknownFailure (MaxCodeSizeExceeded limit actual) -> + UnknownFailure (MaxCodeSizeExceeded limit actual) _ -> "Max code size exceeded. " ++ codeSizeErrorDetails limit actual - UnknownFailure (MaxInitCodeSizeExceeded limit actual) -> + UnknownFailure (MaxInitCodeSizeExceeded limit actual) _ -> "Max init code size exceeded. " ++ codeSizeErrorDetails limit actual - UnknownFailure e -> "VM failed for unhandled reason, " ++ show e + UnknownFailure e trace -> "VM failed for unhandled reason, " ++ show e ++ ". This shouldn't happen. Please file a ticket with this error message and steps to reproduce!" + ++ maybe "" ((" Stack trace:\n" ++) . unpack) trace where codeSizeErrorDetails limit actual = "Configured limit: " ++ show limit ++ ", actual: " ++ show actual From 08041e4b6e327eb258d34124dfd8c83c7ae1ca4e Mon Sep 17 00:00:00 2001 From: Gustavo Grieco <31542053+ggrieco-tob@users.noreply.github.com> Date: Thu, 11 Jul 2024 15:28:59 +0200 Subject: [PATCH 20/29] Initial support for tstore/tload (#1286) * switch to tstore branch on hevm fork. all we should need now is a call to clearTStorages * call clearTStorages after each transaction * test case * bump hevm * bump hevm again --------- Co-authored-by: Sam Alws --- flake.nix | 4 ++-- lib/Echidna/Exec.hs | 3 ++- stack.yaml | 2 +- tstore_test.sol | 24 ++++++++++++++++++++++++ 4 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 tstore_test.sol diff --git a/flake.nix b/flake.nix index a9399895f..b774761bb 100644 --- a/flake.nix +++ b/flake.nix @@ -51,8 +51,8 @@ pkgs.haskellPackages.callCabal2nix "hevm" (pkgs.fetchFromGitHub { owner = "trail-of-forks"; repo = "hevm"; - rev = "2aa7b3e5fea0e0657fe44549ccefbb18f61eb024"; - sha256 = "sha256-/9NMvSOzP0agJ1qEFDN/OQvV0DXRTN3AbntTAzPXbCw="; + rev = "7d4344c5e71d14466e86331af064bab61d06bdad"; + sha256 = "sha256-kts6mdwx5KUrVdNztzewWgNM9xGViAhFIZPnWOUllOU="; }) { secp256k1 = pkgs.secp256k1; }); # FIXME: figure out solc situation, it conflicts with the one from diff --git a/lib/Echidna/Exec.hs b/lib/Echidna/Exec.hs index e6711fe13..99f3aa3ce 100644 --- a/lib/Echidna/Exec.hs +++ b/lib/Echidna/Exec.hs @@ -22,7 +22,7 @@ import Data.Vector qualified as V import Data.Vector.Unboxed.Mutable qualified as VMut import System.Process (readProcessWithExitCode) -import EVM (bytecode, replaceCodeOfSelf, loadContract, exec1, vmOpIx) +import EVM (bytecode, replaceCodeOfSelf, loadContract, exec1, vmOpIx, clearTStorages) import EVM.ABI import EVM.Dapp (DappInfo) import EVM.Exec (exec, vmForEthrunCreation) @@ -98,6 +98,7 @@ execTxWith executeTx tx = do vmResult <- runFully gasLeftAfterTx <- gets (.state.gas) handleErrorsAndConstruction vmResult vmBeforeTx + fromEVM clearTStorages pure (vmResult, gasLeftBeforeTx - gasLeftAfterTx) where runFully = do diff --git a/stack.yaml b/stack.yaml index d09734d4f..d63fc4282 100644 --- a/stack.yaml +++ b/stack.yaml @@ -5,7 +5,7 @@ packages: extra-deps: - git: https://github.com/trail-of-forks/hevm.git - commit: 2aa7b3e5fea0e0657fe44549ccefbb18f61eb024 + commit: 7d4344c5e71d14466e86331af064bab61d06bdad - restless-git-0.7@sha256:346a5775a586f07ecb291036a8d3016c3484ccdc188b574bcdec0a82c12db293,968 - s-cargot-0.1.4.0@sha256:61ea1833fbb4c80d93577144870e449d2007d311c34d74252850bb48aa8c31fb,3525 diff --git a/tstore_test.sol b/tstore_test.sol new file mode 100644 index 000000000..eea83c138 --- /dev/null +++ b/tstore_test.sol @@ -0,0 +1,24 @@ +pragma solidity >=0.8.25; + +contract Test { + uint256 x; + uint256 y; + function A() public { + assembly { + if tload(0) { revert(0,0) } + tstore(0, 1) + if iszero(tload(0)) { revert(0,0) } + } + x = 5; + } + function B() public { + if (x != 5) revert(); + assembly { + if tload(0) { revert(0,0) } + } + y = 10; + } + function echidna_foo() public view returns (bool) { + return y != 10; + } +} From 7bbfd68846d212ecac4f93ddbe1a6492f59aaa1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Jul 2024 10:16:25 +0200 Subject: [PATCH 21/29] Bump sigstore/gh-action-sigstore-python from 2.1.1 to 3.0.0 (#1289) Bumps [sigstore/gh-action-sigstore-python](https://github.com/sigstore/gh-action-sigstore-python) from 2.1.1 to 3.0.0. - [Release notes](https://github.com/sigstore/gh-action-sigstore-python/releases) - [Changelog](https://github.com/sigstore/gh-action-sigstore-python/blob/main/CHANGELOG.md) - [Commits](https://github.com/sigstore/gh-action-sigstore-python/compare/v2.1.1...v3.0.0) --- updated-dependencies: - dependency-name: sigstore/gh-action-sigstore-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d38832264..a25b969a8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -96,7 +96,7 @@ jobs: merge-multiple: true - name: Sign binaries - uses: sigstore/gh-action-sigstore-python@v2.1.1 + uses: sigstore/gh-action-sigstore-python@v3.0.0 with: inputs: ./echidna-*.tar.gz From 6220064678611521595d70f89c0a1be363f861c7 Mon Sep 17 00:00:00 2001 From: omahs <73983677+omahs@users.noreply.github.com> Date: Tue, 16 Jul 2024 10:20:31 +0200 Subject: [PATCH 22/29] Fix typos (#1287) * fix typo * fix typo * fix typo * fix typo --- CONTRIBUTING.md | 2 +- README.md | 2 +- tests/solidity/addressarrayutils/AddressArrayUtils.sol | 2 +- .../addressarrayutils/AddressArrayUtils_withHasDuplicateBug.sol | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fa2fdaf75..4d3849b2a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ appreciate all contributions, including bug reports, feature suggestions, tutorials/blog posts, and code improvements. If you're unsure where to start, we recommend to join our [chat room](https://slack.empirehacking.nyc/) -(in the #ethereum channel) to discuss new ideas to improve this tool. You can also take a look to the [`help wanted`](https://github.com/crytic/echidna/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) +(in the #ethereum channel) to discuss new ideas to improve this tool. You can also take a look at the [`help wanted`](https://github.com/crytic/echidna/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) issue labels. ## Bug reports and feature suggestions diff --git a/README.md b/README.md index 2148c5543..38e6a575c 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,7 @@ Common causes for performance issues that we observed: - Lazy data constructors that accumulate thunks - Inefficient data structures used in hot paths -Checking for these is a good place to start. If you suspect some comuptation is too lazy and +Checking for these is a good place to start. If you suspect some computation is too lazy and leaks memory, you can use `force` from `Control.DeepSeq` to make sure it gets evaluated. ## Limitations and known issues diff --git a/tests/solidity/addressarrayutils/AddressArrayUtils.sol b/tests/solidity/addressarrayutils/AddressArrayUtils.sol index 64d9f2c88..4c151e499 100644 --- a/tests/solidity/addressarrayutils/AddressArrayUtils.sol +++ b/tests/solidity/addressarrayutils/AddressArrayUtils.sol @@ -314,7 +314,7 @@ library AddressArrayUtils { * Returns whether the two arrays are equal. * @param A The first array * @param B The second array - * @return True is the arrays are equal, false if not. + * @return True if the arrays are equal, false if not. */ function isEqual(address[] memory A, address[] memory B) internal pure returns (bool) { if (A.length != B.length) { diff --git a/tests/solidity/addressarrayutils/AddressArrayUtils_withHasDuplicateBug.sol b/tests/solidity/addressarrayutils/AddressArrayUtils_withHasDuplicateBug.sol index 1095cbf65..2375c9e87 100644 --- a/tests/solidity/addressarrayutils/AddressArrayUtils_withHasDuplicateBug.sol +++ b/tests/solidity/addressarrayutils/AddressArrayUtils_withHasDuplicateBug.sol @@ -313,7 +313,7 @@ library AddressArrayUtils { * Returns whether the two arrays are equal. * @param A The first array * @param B The second array - * @return True is the arrays are equal, false if not. + * @return True if the arrays are equal, false if not. */ function isEqual(address[] memory A, address[] memory B) internal pure returns (bool) { if (A.length != B.length) { From 7fe4d401e45a54516745e7f7f932986497673eb9 Mon Sep 17 00:00:00 2001 From: Elias Rad <146735585+nnsW3@users.noreply.github.com> Date: Tue, 16 Jul 2024 11:23:07 +0300 Subject: [PATCH 23/29] Docs improvement (#1278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix README.md * fix Array.hs * fix Corpus.hs * fix Coverage.hs * fix Solidity.hs * fix Tx.hs * fix Report.hs * fix ABI.hs * fix Campaign.hs * fix Exec.hs * fix Onchain.hs --------- Co-authored-by: Emilio López <2642849+elopez@users.noreply.github.com> --- .github/container-linux-static/README.md | 2 +- lib/Echidna/ABI.hs | 2 +- lib/Echidna/Campaign.hs | 2 +- lib/Echidna/Exec.hs | 2 +- lib/Echidna/Mutator/Array.hs | 2 +- lib/Echidna/Onchain.hs | 2 +- lib/Echidna/Output/Corpus.hs | 2 +- lib/Echidna/Types/Coverage.hs | 2 +- lib/Echidna/Types/Solidity.hs | 2 +- lib/Echidna/Types/Tx.hs | 2 +- lib/Echidna/UI/Report.hs | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/container-linux-static/README.md b/.github/container-linux-static/README.md index 1453628f8..011d22349 100644 --- a/.github/container-linux-static/README.md +++ b/.github/container-linux-static/README.md @@ -4,5 +4,5 @@ This container is used as part of `.github/workflows/ci.yml` to produce statically-linked amd64 linux builds of Echidna. It is based on the following container produced by FP Complete and maintained in the [`fpco/alpine-haskell-stack`](https://github.com/fpco/alpine-haskell-stack/tree/ghc927) -repository, and contains a few extra dependencies and configuration to make it +repository, and contains a few extra dependencies and configurations to make it suitable for the GitHub Actions environment. diff --git a/lib/Echidna/ABI.hs b/lib/Echidna/ABI.hs index d3616bdc7..e5269c967 100644 --- a/lib/Echidna/ABI.hs +++ b/lib/Echidna/ABI.hs @@ -211,7 +211,7 @@ addChars c b = foldM withR b . enumFromTo 0 =<< rand where addNulls :: MonadRandom m => ByteString -> m ByteString addNulls = addChars $ pure 0 --- | Given a \"list-y\" structure with analogues of 'take', 'drop', and 'length', +-- | Given a \"list-y\" structure with analogs of 'take', 'drop', and 'length', -- remove some elements at random. shrinkWith :: MonadRandom m diff --git a/lib/Echidna/Campaign.hs b/lib/Echidna/Campaign.hs index 8842dafb1..dad687231 100644 --- a/lib/Echidna/Campaign.hs +++ b/lib/Echidna/Campaign.hs @@ -397,7 +397,7 @@ callseq vm txSeq = do where -- Given a list of transactions and a return typing rule, checks whether we - -- know the return type for each function called. If yes, tries to parse the + -- know the return type for each function called. If yes, try to parse the -- return value as a value of that type. Returns a 'GenDict' style Map. returnValues :: [(Tx, VMResult Concrete RealWorld)] diff --git a/lib/Echidna/Exec.hs b/lib/Echidna/Exec.hs index 99f3aa3ce..fb04d3580 100644 --- a/lib/Echidna/Exec.hs +++ b/lib/Echidna/Exec.hs @@ -132,7 +132,7 @@ execTxWith executeTx tx = do fromEVM (continuation contract) liftIO $ atomicWriteIORef cacheRef $ Map.insert addr (Just contract) cache _ -> do - -- TODO: better error reporting in HEVM, when intermmittent + -- TODO: better error reporting in HEVM, when intermittent -- network error then retry liftIO $ atomicWriteIORef cacheRef $ Map.insert addr Nothing cache logMsg $ "ERROR: Failed to fetch contract: " <> show q diff --git a/lib/Echidna/Mutator/Array.hs b/lib/Echidna/Mutator/Array.hs index a756c1dda..65ac2f1f5 100644 --- a/lib/Echidna/Mutator/Array.hs +++ b/lib/Echidna/Mutator/Array.hs @@ -15,7 +15,7 @@ listMutators = fromList -- | Mutate a list-like data structure using a list of mutators mutateLL :: (LL.ListLike f i, MonadRandom m) - -- | Required size for the mutated list-like value (or Nothing if there are no constrains) + -- | Required size for the mutated list-like value (or Nothing if there are no constraints) => Maybe Int -- | Randomly generated list-like value to complement the mutated list, if it is -- shorter than the requested size diff --git a/lib/Echidna/Onchain.hs b/lib/Echidna/Onchain.hs index d26cfacd1..88e7fa07d 100644 --- a/lib/Echidna/Onchain.hs +++ b/lib/Echidna/Onchain.hs @@ -99,7 +99,7 @@ toFetchedContractData contract = } -- | Try to load the persisted RPC cache. --- TODO: we use the corpus dir for now, think where to place it +-- TODO: we use the corpus dir for now, think about where to place it loadRpcCache :: EConfig -> IO ( Map Addr (Maybe Contract) diff --git a/lib/Echidna/Output/Corpus.hs b/lib/Echidna/Output/Corpus.hs index 4151d14b3..5c2d7d659 100644 --- a/lib/Echidna/Output/Corpus.hs +++ b/lib/Echidna/Output/Corpus.hs @@ -49,7 +49,7 @@ saveCorpusEvent env (_time, campaignEvent) = do getEventInfo = \case -- TODO: We save intermediate reproducers in separate directories. - -- This is to because there can be a lot of them and we want to skip + -- This is because there can be a lot of them and we want to skip -- loading those on startup. Ideally, we should override the same file -- with a better version of a reproducer, this is smaller or more optimized. TestFalsified test -> Just ("reproducers-unshrunk", test.reproducer) diff --git a/lib/Echidna/Types/Coverage.hs b/lib/Echidna/Types/Coverage.hs index 3531a8ddf..8d1241172 100644 --- a/lib/Echidna/Types/Coverage.hs +++ b/lib/Echidna/Types/Coverage.hs @@ -31,7 +31,7 @@ type TxResults = Word64 -- | Given good point coverage, count the number of unique points but -- only considering the different instruction PCs (discarding the TxResult). --- This is useful to report a coverage measure to the user +-- This is useful for reporting a coverage measure to the user scoveragePoints :: CoverageMap -> IO Int scoveragePoints cm = do sum <$> mapM (V.foldl' countCovered 0) (Map.elems cm) diff --git a/lib/Echidna/Types/Solidity.hs b/lib/Echidna/Types/Solidity.hs index f17e81697..f96076332 100644 --- a/lib/Echidna/Types/Solidity.hs +++ b/lib/Echidna/Types/Solidity.hs @@ -50,7 +50,7 @@ instance Show SolException where ConstructorArgs s -> "Constructor arguments are required: " ++ s NoCryticCompile -> "crytic-compile not installed or not found in PATH. To install it, run:\n pip install crytic-compile" InvalidMethodFilters f -> "Applying the filter " ++ show f ++ " to the methods produces an empty list. Are you filtering the correct functions using `filterFunctions` or fuzzing the correct contract?" - SetUpCallFailed -> "Calling the setUp() function failed (revert, out-of-gas, sending ether to an non-payable constructor, etc.)" + SetUpCallFailed -> "Calling the setUp() function failed (revert, out-of-gas, sending ether to a non-payable constructor, etc.)" DeploymentFailed a t -> "Deploying the contract " ++ show a ++ " failed (revert, out-of-gas, sending ether to an non-payable constructor, etc.):\n" ++ unpack t OutdatedSolcVersion v -> "Solc version " ++ toString v ++ " detected. Echidna doesn't support versions of solc before " ++ toString minSupportedSolcVersion ++ ". Please use a newer version." diff --git a/lib/Echidna/Types/Tx.hs b/lib/Echidna/Types/Tx.hs index 11c1372cc..ea844eec0 100644 --- a/lib/Echidna/Types/Tx.hs +++ b/lib/Echidna/Types/Tx.hs @@ -218,7 +218,7 @@ catNoCalls (tx1:tx2:xs) = _ -> tx1 : catNoCalls (tx2:xs) where nc = tx1 { delay = (fst tx1.delay + fst tx2.delay, snd tx1.delay + snd tx2.delay) } --- | Transform a VMResult into a more hash friendly sum type +-- | Transform a VMResult into a more hash-friendly sum type getResult :: VMResult Concrete s -> TxResult getResult = \case VMSuccess b | forceBuf b == encodeAbiValue (AbiBool True) -> ReturnTrue diff --git a/lib/Echidna/UI/Report.hs b/lib/Echidna/UI/Report.hs index 23ee7bb2f..580267b67 100644 --- a/lib/Echidna/UI/Report.hs +++ b/lib/Echidna/UI/Report.hs @@ -59,7 +59,7 @@ ppCampaign vm workerStates = do , seedPrinted ] --- | Given rules for pretty-printing associated address, and whether to print +-- | Given rules for pretty-printing associated addresses, and whether to print -- them, pretty-print a 'Transaction'. ppTx :: MonadReader Env m => VM Concrete RealWorld -> Bool -> Tx -> m String ppTx _ _ Tx { call = NoCall, delay } = From 3b5d6d93a501d6e9cd649c51dfdb08361be83412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20L=C3=B3pez?= <2642849+elopez@users.noreply.github.com> Date: Tue, 16 Jul 2024 14:08:19 +0200 Subject: [PATCH 24/29] Raise default number of workers (#1288) Now the default number of workers will be equal to the number of cores on the system, with a minimum of 1 and a maximum of 4. --- lib/Echidna/Types/Campaign.hs | 11 ++++++++--- tests/solidity/basic/default.yaml | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/Echidna/Types/Campaign.hs b/lib/Echidna/Types/Campaign.hs index 78a885189..b54c19185 100644 --- a/lib/Echidna/Types/Campaign.hs +++ b/lib/Echidna/Types/Campaign.hs @@ -3,10 +3,10 @@ module Echidna.Types.Campaign where import Control.Concurrent (ThreadId) import Data.Aeson import Data.Map (Map) -import Data.Maybe (fromMaybe) import Data.Text (Text) import Data.Text qualified as T import Data.Word (Word8, Word16) +import GHC.Conc (numCapabilities) import Echidna.ABI (GenDict, emptyDict, encodeSig) import Echidna.Types @@ -211,9 +211,14 @@ defaultSymExecAskSMTIters :: Integer defaultSymExecAskSMTIters = 1 -- | Get number of fuzzing workers (doesn't include sym exec worker) --- Defaults to 1 if set to Nothing +-- Defaults to `N` if set to Nothing, where `N` is Haskell's -N value, +-- usually the number of cores, clamped between 1 and 4. getNFuzzWorkers :: CampaignConf -> Int -getNFuzzWorkers conf = fromIntegral (fromMaybe 1 (conf.workers)) +getNFuzzWorkers conf = maybe defaultN fromIntegral conf.workers + where + n = numCapabilities + maxN = max 1 n + defaultN = min 4 maxN -- capped at 4 by default -- | Number of workers, including SymExec worker if there is one getNWorkers :: CampaignConf -> Int diff --git a/tests/solidity/basic/default.yaml b/tests/solidity/basic/default.yaml index d9c277287..fc52ab1cc 100644 --- a/tests/solidity/basic/default.yaml +++ b/tests/solidity/basic/default.yaml @@ -89,8 +89,8 @@ rpcUrl: null rpcBlock: null # Etherscan API key etherscanApiKey: null -# number of workers -workers: 1 +# number of workers. By default (unset) its value is the clamp of the number cores between 1 and 4 +workers: null # events server port server: null # whether to add an additional symbolic execution worker From 84702aabd0c2d550079a95976a184932b0973082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20L=C3=B3pez?= <2642849+elopez@users.noreply.github.com> Date: Tue, 16 Jul 2024 16:20:35 +0200 Subject: [PATCH 25/29] Echidna 2.2.4 (#1291) --- CHANGELOG.md | 11 +++++++++++ package.yaml | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae4923ffc..eaf8d4913 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## 2.2.4 + +* Initial TLOAD/TSTORE support (#1286) +* Initial symbolic execution implementation (experimental, #1216, #1251, #1285) +* New panel toggles in the UI (#1197) +* New gas/s metric (#1279) +* Improved logging (#1202, #1271, #1273, #1274, #1283, #1258, #1269) +* Performance improvements with multiple workers (#1228) +* Shrinking improvements (#1196, #1250, #1280) +* Improved configuration options (#1200, #1227, #1251) + ## 2.2.3 * feat: add CLI commands for RPC URL and block number (#1194) diff --git a/package.yaml b/package.yaml index 191eff522..2e4cc7093 100644 --- a/package.yaml +++ b/package.yaml @@ -3,7 +3,7 @@ name: echidna author: Trail of Bits maintainer: Trail of Bits -version: 2.2.3 +version: 2.2.4 ghc-options: - -O2 From f8deef5b791f93eacf3c1689d21ed15935b983c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20L=C3=B3pez?= <2642849+elopez@users.noreply.github.com> Date: Tue, 16 Jul 2024 17:21:32 +0200 Subject: [PATCH 26/29] Fix sigstore file attachments (#1292) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a25b969a8..29fbef5b9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -107,4 +107,4 @@ jobs: name: "Echidna ${{ needs.nixBuild.outputs.version }}" files: | ./echidna-*.tar.gz - ./echidna-*.tar.gz.sigstore + ./echidna-*.tar.gz.sigstore.json From 9893a09ae9f838c5de8d7fac71886598308a6a7a Mon Sep 17 00:00:00 2001 From: Gustavo Grieco <31542053+ggrieco-tob@users.noreply.github.com> Date: Thu, 18 Jul 2024 14:00:35 +0200 Subject: [PATCH 27/29] IllegalOverflow should be similar to a revert instead of a VM error (#1293) --- lib/Echidna/Exec.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Echidna/Exec.hs b/lib/Echidna/Exec.hs index fb04d3580..f76691897 100644 --- a/lib/Echidna/Exec.hs +++ b/lib/Echidna/Exec.hs @@ -54,7 +54,7 @@ classifyError = \case StackLimitExceeded -> RevertE StackUnderrun -> IllegalE BadJumpDestination -> IllegalE - IllegalOverflow -> IllegalE + IllegalOverflow -> RevertE _ -> UnknownE -- | Extracts the 'Query' if there is one. From e7c17fe513ef3c89e9c24af34a7191b190ae9efd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jul 2024 11:02:51 +0200 Subject: [PATCH 28/29] Bump DeterminateSystems/nix-installer-action from 12 to 13 (#1294) Bumps [DeterminateSystems/nix-installer-action](https://github.com/determinatesystems/nix-installer-action) from 12 to 13. - [Release notes](https://github.com/determinatesystems/nix-installer-action/releases) - [Commits](https://github.com/determinatesystems/nix-installer-action/compare/v12...v13) --- updated-dependencies: - dependency-name: DeterminateSystems/nix-installer-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 29fbef5b9..c060563b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,7 +36,7 @@ jobs: uses: actions/checkout@v4 - name: Install Nix - uses: DeterminateSystems/nix-installer-action@v12 + uses: DeterminateSystems/nix-installer-action@v13 - name: Configure Cachix uses: cachix/cachix-action@v15 From a55009409e0e312f015240f4d2f02a5f58ce3687 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jul 2024 11:03:02 +0200 Subject: [PATCH 29/29] Bump softprops/action-gh-release from 2.0.6 to 2.0.8 (#1295) Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.0.6 to 2.0.8. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/v2.0.6...v2.0.8) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c060563b0..adf25ec35 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -101,7 +101,7 @@ jobs: inputs: ./echidna-*.tar.gz - name: Create GitHub release and upload binaries - uses: softprops/action-gh-release@v2.0.6 + uses: softprops/action-gh-release@v2.0.8 with: draft: true name: "Echidna ${{ needs.nixBuild.outputs.version }}"