diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 072242cfe..002b0f833 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,10 +16,8 @@ jobs: matrix: include: - os: ubuntu-20.04 - apt-get: autoconf automake libtool shell: bash - os: macos-latest - brew: automake shell: bash - os: windows-latest shell: msys2 {0} @@ -29,12 +27,9 @@ jobs: shell: ${{ matrix.shell }} steps: - - name: Get Packages - uses: mstksg/get-package@v1 - if: runner.os != 'Windows' - with: - brew: ${{ matrix.brew }} - apt-get: ${{ matrix.apt-get }} + - name: Get Packages (macOS) + if: runner.os == 'macOS' + run: brew install automake - name: Get Packages (Windows) uses: msys2/setup-msys2@v2 @@ -72,19 +67,21 @@ jobs: uses: actions/cache@v3 with: path: ~/.local/ - key: ${{ runner.os }}-local-v4 + key: ${{ runner.os }}-local-v5-${{ hashFiles('.github/scripts/install-*') }} - name: Cache Stack uses: actions/cache@v3 with: - path: ~/.stack - key: ${{ runner.os }}-stack-v4 + path: | + ~/.stack + .stack-work + key: ${{ runner.os }}-stack-v5-${{ hashFiles('package.yaml', 'stack.yaml') }} - name: Cache Cabal uses: actions/cache@v3 with: path: ~/.cabal - key: ${{ runner.os }}-cabal-v4 + key: ${{ runner.os }}-cabal-v5-${{ hashFiles('package.yaml', 'stack.yaml') }} - name: Build Libraries run: | diff --git a/lib/Echidna/Campaign.hs b/lib/Echidna/Campaign.hs index 762e4e60e..4ccfc5bbb 100644 --- a/lib/Echidna/Campaign.hs +++ b/lib/Echidna/Campaign.hs @@ -24,26 +24,27 @@ import Data.Text (Text) import System.Random (mkStdGen) import EVM (Contract, VM(..), VMResult(..), bytecode) -import qualified EVM (Env(..)) +import EVM qualified (Env(..)) import EVM.ABI (getAbi, AbiType(AbiAddressType), AbiValue(AbiAddress)) import EVM.Types (Addr, Expr(ConcreteBuf)) import Echidna.ABI +import Echidna.Events (extractEvents) import Echidna.Exec +import Echidna.Mutator.Corpus +import Echidna.Shrink (shrinkTest) import Echidna.Test import Echidna.Transaction -import Echidna.Shrink (shrinkTest) +import Echidna.Types (Gas) +import Echidna.Types.Buffer (viewBuffer) import Echidna.Types.Campaign import Echidna.Types.Config import Echidna.Types.Corpus (InitialCorpus) import Echidna.Types.Coverage (coveragePoints) -import Echidna.Types.Test -import Echidna.Types.Buffer (viewBuffer) import Echidna.Types.Signature (makeBytecodeMemo) +import Echidna.Types.Test import Echidna.Types.Tx (TxCall(..), Tx(..), getResult, call) import Echidna.Types.World (World) -import Echidna.Mutator.Corpus -import Echidna.Events (extractEvents) instance MonadThrow m => MonadThrow (RandT g m) where throwM = lift . throwM @@ -120,7 +121,7 @@ evalSeq vmForShrink e = go [] where -- | Given current `gasInfo` and a sequence of executed transactions, updates information on highest -- gas usage for each call -updateGasInfo :: [(Tx, (VMResult, Int))] -> [Tx] -> Map Text (Int, [Tx]) -> Map Text (Int, [Tx]) +updateGasInfo :: [(Tx, (VMResult, Gas))] -> [Tx] -> Map Text (Gas, [Tx]) -> Map Text (Gas, [Tx]) updateGasInfo [] _ gi = gi updateGasInfo ((t@(Tx (SolCall (f, _)) _ _ _ _ _ _), (_, used')):ts) tseq gi = case mused of @@ -135,7 +136,7 @@ updateGasInfo ((t, _):ts) tseq gi = updateGasInfo ts (t:tseq) gi -- | Execute a transaction, capturing the PC and codehash of each instruction executed, saving the -- transaction if it finds new coverage. -execTxOptC :: (MonadIO m, MonadState (VM, Campaign) m, MonadThrow m) => Tx -> m (VMResult, Int) +execTxOptC :: (MonadIO m, MonadState (VM, Campaign) m, MonadThrow m) => Tx -> m (VMResult, Gas) execTxOptC tx = do (vm, Campaign{_bcMemo, _coverage = oldCov}) <- get let cov = _2 . coverage @@ -155,7 +156,7 @@ execTxOptC tx = do return res -- | Given a list of transactions in the corpus, save them discarding reverted transactions -addToCorpus :: MonadState Campaign m => Int -> [(Tx, (VMResult, Int))] -> m () +addToCorpus :: MonadState Campaign m => Int -> [(Tx, (VMResult, Gas))] -> m () addToCorpus n res = unless (null rtxs) $ corpus %= Set.insert (n, rtxs) where rtxs = fst <$> res diff --git a/lib/Echidna/Config.hs b/lib/Echidna/Config.hs index 076a88cc1..d5a265ee3 100644 --- a/lib/Echidna/Config.hs +++ b/lib/Echidna/Config.hs @@ -1,7 +1,6 @@ module Echidna.Config where -import Control.Lens -import Control.Monad.Fail qualified as M (MonadFail(..)) +import Control.Applicative ((<|>)) import Control.Monad.Reader (Reader, ReaderT(..), runReader) import Control.Monad.State (StateT(..), runStateT, modify') import Control.Monad.Trans (lift) @@ -9,6 +8,7 @@ import Data.Aeson import Data.Aeson.KeyMap (keys) import Data.Bool (bool) import Data.ByteString qualified as BS +import Data.Functor ((<&>)) import Data.HashSet (fromList, insert, difference) import Data.Maybe (fromMaybe) import Data.Set qualified as Set @@ -38,94 +38,102 @@ instance FromJSON EConfigWithUsage where -- config and not used and which keys were unset in the config and defaulted parseJSON o = do let v' = case o of - Object v -> v - _ -> mempty + Object v -> v + _ -> mempty (c, ks) <- runStateT (parser v') $ fromList [] let found = fromList (keys v') - return $ EConfigWithUsage c (found `difference` ks) (ks `difference` found) + pure $ EConfigWithUsage c (found `difference` ks) (ks `difference` found) -- this parser runs in StateT and comes equipped with the following -- equivalent unary operators: -- x .:? k (Parser) <==> x ..:? k (StateT) -- x .!= v (Parser) <==> x ..!= v (StateT) -- tl;dr use an extra initial . to lift into the StateT parser - where parser v = - let useKey k = modify' $ insert k - x ..:? k = useKey k >> lift (x .:? k) - x ..!= y = fromMaybe y <$> x - -- Parse as unbounded Integer and see if it fits into W256 - getWord256 k def = do - value :: Integer <- fromMaybe (fromIntegral (def :: W256)) <$> v ..:? k - if value > fromIntegral (maxBound :: W256) then - fail $ show k <> ": value does not fit in 256 bits" - else - pure $ fromIntegral value + where + parser v = + EConfig <$> campaignConfParser + <*> pure names + <*> solConfParser + <*> testConfParser + <*> txConfParser + <*> (UIConf <$> v ..:? "timeout" <*> formatParser) + where + useKey k = modify' $ insert k + x ..:? k = useKey k >> lift (x .:? k) + x ..!= y = fromMaybe y <$> x + -- Parse as unbounded Integer and see if it fits into W256 + getWord256 k def = do + value :: Integer <- fromMaybe (fromIntegral (def :: W256)) <$> v ..:? k + if value > fromIntegral (maxBound :: W256) then + fail $ show k <> ": value does not fit in 256 bits" + else + pure $ fromIntegral value - -- TxConf - xc = TxConf <$> v ..:? "propMaxGas" ..!= maxGasPerBlock - <*> v ..:? "testMaxGas" ..!= maxGasPerBlock - <*> getWord256 "maxGasprice" 0 - <*> getWord256 "maxTimeDelay" defaultTimeDelay - <*> getWord256 "maxBlockDelay" defaultBlockDelay - <*> getWord256 "maxValue" 100000000000000000000 -- 100 eth + txConfParser = TxConf + <$> v ..:? "propMaxGas" ..!= maxGasPerBlock + <*> v ..:? "testMaxGas" ..!= maxGasPerBlock + <*> getWord256 "maxGasprice" 0 + <*> getWord256 "maxTimeDelay" defaultTimeDelay + <*> getWord256 "maxBlockDelay" defaultBlockDelay + <*> getWord256 "maxValue" 100000000000000000000 -- 100 eth - -- TestConf - tc = do - psender <- v ..:? "psender" ..!= 0x10000 - fprefix <- v ..:? "prefix" ..!= "echidna_" - let goal fname = if (fprefix <> "revert_") `isPrefixOf` fname then ResRevert else ResTrue - classify fname vm = maybe ResOther classifyRes vm._result == goal fname - return $ TestConf classify (const psender) + testConfParser = do + psender <- v ..:? "psender" ..!= 0x10000 + fprefix <- v ..:? "prefix" ..!= "echidna_" + let goal fname = if (fprefix <> "revert_") `isPrefixOf` fname then ResRevert else ResTrue + classify fname vm = maybe ResOther classifyRes vm._result == goal fname + pure $ TestConf classify (const psender) - -- CampaignConf - cov = v ..:? "coverage" <&> \case Just False -> Nothing - _ -> Just mempty - cc = CampaignConf <$> v ..:? "testLimit" ..!= defaultTestLimit - <*> v ..:? "stopOnFail" ..!= False - <*> v ..:? "estimateGas" ..!= False - <*> v ..:? "seqLen" ..!= defaultSequenceLength - <*> v ..:? "shrinkLimit" ..!= defaultShrinkLimit - <*> cov - <*> v ..:? "seed" - <*> v ..:? "dictFreq" ..!= 0.40 - <*> v ..:? "corpusDir" ..!= Nothing - <*> v ..:? "mutConsts" ..!= defaultMutationConsts + campaignConfParser = CampaignConf + <$> v ..:? "testLimit" ..!= defaultTestLimit + <*> v ..:? "stopOnFail" ..!= False + <*> v ..:? "estimateGas" ..!= False + <*> v ..:? "seqLen" ..!= defaultSequenceLength + <*> v ..:? "shrinkLimit" ..!= defaultShrinkLimit + <*> (v ..:? "coverage" <&> \case Just False -> Nothing; _ -> Just mempty) + <*> v ..:? "seed" + <*> v ..:? "dictFreq" ..!= 0.40 + <*> v ..:? "corpusDir" ..!= Nothing + <*> v ..:? "mutConsts" ..!= defaultMutationConsts - -- SolConf - fnFilter = bool Whitelist Blacklist <$> v ..:? "filterBlacklist" ..!= True - <*> v ..:? "filterFunctions" ..!= [] - mode = v ..:? "testMode" >>= \case - Just s -> pure $ validateTestMode s - Nothing -> pure "property" - sc = SolConf <$> v ..:? "contractAddr" ..!= defaultContractAddr - <*> v ..:? "deployer" ..!= defaultDeployerAddr - <*> v ..:? "sender" ..!= Set.fromList [0x10000, 0x20000, defaultDeployerAddr] - <*> v ..:? "balanceAddr" ..!= 0xffffffff - <*> v ..:? "balanceContract" ..!= 0 - <*> v ..:? "codeSize" ..!= 0x6000 -- 24576 (EIP-170) - <*> v ..:? "prefix" ..!= "echidna_" - <*> v ..:? "cryticArgs" ..!= [] - <*> v ..:? "solcArgs" ..!= "" - <*> v ..:? "solcLibs" ..!= [] - <*> v ..:? "quiet" ..!= False - <*> v ..:? "initialize" ..!= Nothing - <*> v ..:? "deployContracts" ..!= [] - <*> v ..:? "deployBytecodes" ..!= [] - <*> v ..:? "allContracts" ..!= False - <*> mode - <*> v ..:? "testDestruction" ..!= False - <*> v ..:? "allowFFI" ..!= False - <*> fnFilter - names :: Names - names Sender = (" from: " ++) . show - names _ = const "" - format = fromMaybe Interactive <$> (v ..:? "format" >>= \case - Just ("text" :: String) -> pure . Just . NonInteractive $ Text - Just "json" -> pure . Just . NonInteractive $ JSON - Just "none" -> pure . Just . NonInteractive $ None - Nothing -> pure Nothing - _ -> M.fail "Unrecognized format type (should be text, json, or none)") in - EConfig <$> cc <*> pure names <*> sc <*> tc <*> xc - <*> (UIConf <$> v ..:? "timeout" <*> format) + solConfParser = SolConf + <$> v ..:? "contractAddr" ..!= defaultContractAddr + <*> v ..:? "deployer" ..!= defaultDeployerAddr + <*> v ..:? "sender" ..!= Set.fromList [0x10000, 0x20000, defaultDeployerAddr] + <*> v ..:? "balanceAddr" ..!= 0xffffffff + <*> v ..:? "balanceContract" ..!= 0 + <*> v ..:? "codeSize" ..!= 0x6000 -- 24576 (EIP-170) + <*> v ..:? "prefix" ..!= "echidna_" + <*> v ..:? "cryticArgs" ..!= [] + <*> v ..:? "solcArgs" ..!= "" + <*> v ..:? "solcLibs" ..!= [] + <*> v ..:? "quiet" ..!= False + <*> v ..:? "initialize" ..!= Nothing + <*> v ..:? "deployContracts" ..!= [] + <*> v ..:? "deployBytecodes" ..!= [] + <*> ((<|>) <$> v ..:? "allContracts" + -- TODO: keep compatible with the old name for a while + <*> lift (v .:? "multi-abi")) ..!= False + <*> mode + <*> v ..:? "testDestruction" ..!= False + <*> v ..:? "allowFFI" ..!= False + <*> fnFilter + where + mode = v ..:? "testMode" >>= \case + Just s -> pure $ validateTestMode s + Nothing -> pure "property" + fnFilter = bool Whitelist Blacklist <$> v ..:? "filterBlacklist" ..!= True + <*> v ..:? "filterFunctions" ..!= [] + + names :: Names + names Sender = (" from: " ++) . show + names _ = const "" + + formatParser = fromMaybe Interactive <$> (v ..:? "format" >>= \case + Just ("text" :: String) -> pure . Just . NonInteractive $ Text + Just "json" -> pure . Just . NonInteractive $ JSON + Just "none" -> pure . Just . NonInteractive $ None + Nothing -> pure Nothing + _ -> fail "Unrecognized format type (should be text, json, or none)") -- | The default config used by Echidna (see the 'FromJSON' instance for values used). defaultConfig :: EConfig diff --git a/lib/Echidna/Exec.hs b/lib/Echidna/Exec.hs index bbacf6411..8e085cab7 100644 --- a/lib/Echidna/Exec.hs +++ b/lib/Echidna/Exec.hs @@ -21,7 +21,7 @@ import System.Process (readProcessWithExitCode) import Echidna.Events (emptyEvents) import Echidna.Transaction -import Echidna.Types (ExecException(..), fromEVM) +import Echidna.Types (ExecException(..), Gas, fromEVM) import Echidna.Types.Buffer (viewBuffer) import Echidna.Types.Coverage (CoverageMap) import Echidna.Types.Signature (BytecodeMemo, lookupBytecodeMetadata) @@ -63,7 +63,7 @@ vmExcept e = throwM $ case VMFailure e of {Illegal -> IllegalExec e; _ -> Unknow -- | Given an error handler `onErr`, an execution strategy `executeTx`, and a transaction `tx`, -- execute that transaction using the given execution strategy, calling `onErr` on errors. -execTxWith :: (MonadIO m, MonadState s m) => Lens' s VM -> (Error -> m ()) -> m VMResult -> Tx -> m (VMResult, Int) +execTxWith :: (MonadIO m, MonadState s m) => Lens' s VM -> (Error -> m ()) -> m VMResult -> Tx -> m (VMResult, Gas) execTxWith l onErr executeTx tx = do vm <- use l if hasSelfdestructed vm tx.dst then @@ -76,7 +76,7 @@ execTxWith l onErr executeTx tx = do vmResult <- runFully gasLeftAfterTx <- use $ l . state . gas handleErrorsAndConstruction vmResult vmBeforeTx - pure (vmResult, fromIntegral $ gasLeftBeforeTx - gasLeftAfterTx) + pure (vmResult, gasLeftBeforeTx - gasLeftAfterTx) where runFully = do vmResult <- executeTx @@ -135,7 +135,7 @@ execTxWith l onErr executeTx tx = do _ -> pure () -- | Execute a transaction "as normal". -execTx :: (MonadIO m, MonadState VM m, MonadThrow m) => Tx -> m (VMResult, Int) +execTx :: (MonadIO m, MonadState VM m, MonadThrow m) => Tx -> m (VMResult, Gas) execTx = execTxWith id vmExcept $ fromEVM exec -- | Execute a transaction, logging coverage at every step. @@ -143,7 +143,7 @@ execTxWithCov :: (MonadIO m, MonadState VM m, MonadThrow m) => BytecodeMemo -> Tx - -> m ((VMResult, Int), CoverageMap) + -> m ((VMResult, Gas), CoverageMap) execTxWithCov memo tx = do vm <- get (r, (vm', cm)) <- runStateT (execTxWith _1 vmExcept execCov tx) (vm, mempty) diff --git a/lib/Echidna/Output/JSON.hs b/lib/Echidna/Output/JSON.hs index 6103eea4d..4a672ed0c 100644 --- a/lib/Echidna/Output/JSON.hs +++ b/lib/Echidna/Output/JSON.hs @@ -15,6 +15,7 @@ import Numeric (showHex) import EVM.Types (keccak') import Echidna.ABI (ppAbiValue, GenDict(..)) +import Echidna.Types (Gas) import Echidna.Types.Coverage (CoverageInfo) import Echidna.Types.Campaign qualified as C import Echidna.Types.Test qualified as T @@ -27,7 +28,7 @@ data Campaign = Campaign , _tests :: [Test] , seed :: Int , coverage :: Map String [CoverageInfo] - , gasInfo :: [(Text, (Int, [Tx]))] + , gasInfo :: [(Text, (Gas, [Tx]))] } instance ToJSON Campaign where diff --git a/lib/Echidna/Processor.hs b/lib/Echidna/Processor.hs index 84a6cdf4d..bca437064 100644 --- a/lib/Echidna/Processor.hs +++ b/lib/Echidna/Processor.hs @@ -4,7 +4,7 @@ module Echidna.Processor where import Control.Exception (Exception) import Control.Monad.Catch (MonadThrow(..)) -import Data.Aeson ((.:), (.:?), (.!=), decode, parseJSON, withEmbeddedJSON, withObject) +import Data.Aeson ((.:), (.:?), (.!=), eitherDecode, parseJSON, withEmbeddedJSON, withObject) import Data.Aeson.Types (FromJSON, Parser, Value(String)) import Data.ByteString.Base16 qualified as BS16 (decode) import Data.ByteString.Lazy.Char8 qualified as BSL @@ -107,30 +107,18 @@ instance FromJSON SlitherInfo where v <- o .: "value" t <- o .: "type" case t of - 'u':'i':'n':'t':x -> - case AbiUInt <$> readMaybe x <*> readMaybe v of - Nothing -> failure v t - i -> pure i - - 'i':'n':'t':x -> - case AbiInt <$> readMaybe x <*> readMaybe v of - Nothing -> failure v t - i -> pure i - + 'u':'i':'n':'t':x -> pure $ AbiUInt <$> readMaybe x <*> readMaybe v + 'i':'n':'t':x -> pure $ AbiInt <$> readMaybe x <*> readMaybe v "string" -> pure . Just . AbiString $ if "0x" `isPrefixOf` v then fromRight (error ("invalid b16 decoding of: " ++ show v)) $ BS16.decode $ BSU.fromString $ drop 2 v else BSU.fromString v - "address" -> - case AbiAddress . Addr <$> readMaybe v of - Nothing -> failure v t - a -> pure a + "address" -> pure $ AbiAddress . Addr <$> readMaybe v -- we don't need all the types for now _ -> pure Nothing - where failure v t = fail $ "failed to parse " ++ t ++ ": " ++ v -- Slither processing runSlither :: FilePath -> [String] -> IO SlitherInfo @@ -143,7 +131,7 @@ runSlither fp extraArgs = if ".vy" `isSuffixOf` pack fp then return noInfo else (ec, out, err) <- readCreateProcessWithExitCode (proc path args) {std_err = Inherit} "" case ec of ExitSuccess -> - case decode (BSL.pack out) of - Just si -> pure si - Nothing -> throwM $ ProcessorFailure "slither" "decoding slither output failed" + case eitherDecode (BSL.pack out) of + Right si -> pure si + Left msg -> throwM $ ProcessorFailure "slither" ("decoding slither output failed:\n" ++ msg) ExitFailure _ -> throwM $ ProcessorFailure "slither" err diff --git a/lib/Echidna/Solidity.hs b/lib/Echidna/Solidity.hs index 22f9249ff..f03912ee2 100644 --- a/lib/Echidna/Solidity.hs +++ b/lib/Echidna/Solidity.hs @@ -1,6 +1,6 @@ module Echidna.Solidity where -import Control.Lens +import Control.Lens hiding (filtered) import Control.Arrow (first) import Control.Monad (when, unless, forM_) import Control.Monad.Catch (MonadThrow(..)) @@ -26,7 +26,7 @@ import System.FilePath (joinPath, splitDirectories, ()) import System.IO (openFile, IOMode(..)) import System.Info (os) -import EVM hiding (contracts, path) +import EVM hiding (contract, contracts, path) import EVM qualified (contracts) import EVM.ABI import EVM.Solidity @@ -135,14 +135,12 @@ linkLibraries ls = "--libraries " ++ iconcatMap (\i x -> concat [x, ":", show $ addrLibrary + toEnum i, ","]) ls -- | Filter methods using a whitelist/blacklist -filterMethods :: Text -> Filter -> NE.NonEmpty SolSignature -> NE.NonEmpty SolSignature -filterMethods _ f@(Whitelist []) _ = error $ show $ InvalidMethodFilters f -filterMethods cn f@(Whitelist ic) ms = case NE.filter (\s -> encodeSigWithName cn s `elem` ic) ms of - [] -> error $ show $ InvalidMethodFilters f - fs -> NE.fromList fs -filterMethods cn f@(Blacklist ig) ms = case NE.filter (\s -> encodeSigWithName cn s `notElem` ig) ms of - [] -> error $ show $ InvalidMethodFilters f - fs -> NE.fromList fs +filterMethods :: Text -> Filter -> NE.NonEmpty SolSignature -> [SolSignature] +filterMethods _ f@(Whitelist []) _ = error $ show $ InvalidMethodFilters f +filterMethods contractName (Whitelist ic) ms = + NE.filter (\s -> encodeSigWithName contractName s `elem` ic) ms +filterMethods contractName (Blacklist ig) ms = + NE.filter (\s -> encodeSigWithName contractName s `notElem` ig) ms -- | Filter methods with arguments, used for dapptest mode filterMethodsWithArgs :: NE.NonEmpty SolSignature -> NE.NonEmpty SolSignature @@ -161,7 +159,7 @@ abiOf pref solcContract = -- testing and extract an ABI and list of tests. Throws exceptions if anything returned doesn't look -- usable for Echidna. NOTE: Contract names passed to this function should be prefixed by the -- filename their code is in, plus a colon. -loadSpecified :: SolConf -> Maybe Text -> [SolcContract] -> IO (VM, EventMap, NE.NonEmpty SolSignature, [Text], SignatureMap) +loadSpecified :: SolConf -> Maybe Text -> [SolcContract] -> IO (VM, EventMap, [SolSignature], [Text], SignatureMap) loadSpecified solConf name cs = do -- Pick contract to load c <- choose cs name @@ -170,7 +168,7 @@ loadSpecified solConf name cs = do unless solConf.quiet . putStrLn $ "Analyzing contract: " <> T.unpack c.contractName -- Local variables - let SolConf ca d ads bala balc mcs pref _ _ libs _ fp dpc dpb ma tm _ ffi fs = solConf + let SolConf ca d ads bala balc mcs pref _ _ libs _ fp dpc dpb _ tm _ ffi fs = solConf -- generate the complete abi mapping let bc = c.creationCode @@ -179,13 +177,21 @@ loadSpecified solConf name cs = do -- Filter ABI according to the config options - let fabiOfc = if isDapptestMode tm then filterMethodsWithArgs (abiOf pref c) else filterMethods c.contractName fs $ abiOf pref c + let fabiOfc = if isDapptestMode tm + then NE.toList $ filterMethodsWithArgs (abiOf pref c) + else filterMethods c.contractName fs $ abiOf pref c -- Filter again for dapptest tests or assertions checking if enabled let neFuns = filterMethods c.contractName fs (fallback NE.:| funs) -- Construct ABI mapping for World - let abiMapping = if ma then M.fromList $ cs <&> \cc -> (getBytecodeMetadata cc.runtimeCode, filterMethods cc.contractName fs $ abiOf pref cc) - else M.singleton (getBytecodeMetadata c.runtimeCode) fabiOfc - + let abiMapping = + if solConf.allContracts then + M.fromList $ catMaybes $ cs <&> \contract -> + let filtered = filterMethods contract.contractName fs $ abiOf pref contract + in (getBytecodeMetadata contract.runtimeCode,) <$> NE.nonEmpty filtered + else + case NE.nonEmpty fabiOfc of + Just ne -> M.singleton (getBytecodeMetadata c.runtimeCode) ne + Nothing -> mempty -- Set up initial VM, either with chosen contract or Etheno initialization file -- need to use snd to add to ABI dict @@ -251,25 +257,24 @@ loadSpecified solConf name cs = do -- 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. -loadWithCryticCompile :: SolConf -> NE.NonEmpty FilePath -> Maybe Text -> IO (VM, EventMap, NE.NonEmpty SolSignature, [Text], SignatureMap) +loadWithCryticCompile :: SolConf -> NE.NonEmpty FilePath -> Maybe Text -> IO (VM, EventMap, [SolSignature], [Text], SignatureMap) loadWithCryticCompile solConf fp name = contracts solConf fp >>= \(cs, _) -> loadSpecified solConf name cs -- | Given the results of 'loadSolidity', assuming a single-contract test, get everything ready -- for running a 'Campaign' against the tests found. prepareForTest :: SolConf - -> (VM, EventMap, NE.NonEmpty SolSignature, [Text], SignatureMap) + -> (VM, EventMap, [SolSignature], [Text], SignatureMap) -> Maybe ContractName -> SlitherInfo -> (VM, World, [EchidnaTest]) prepareForTest SolConf{sender, testMode, testDestruction} (vm, em, a, ts, m) c si = do let r = vm._state._contract - a' = NE.toList a ps = filterResults c si.payableFunctions as = if isAssertionMode testMode then filterResults c si.asserts else [] cs = if isDapptestMode testMode then [] else filterResults c si.constantFunctions \\ as (hm, lm) = prepareHashMaps cs as $ filterFallbacks c si.fallbackDefined si.receiveDefined m - (vm, World sender hm lm ps em, createTests testMode testDestruction ts r a') + (vm, World sender hm lm ps em, createTests testMode testDestruction ts r a) filterFallbacks :: Maybe ContractName -> [ContractName] -> [ContractName] -> SignatureMap -> SignatureMap @@ -280,12 +285,11 @@ filterFallbacks _ [] [] sm = M.map f sm filterFallbacks _ _ _ sm = sm -- this limited variant is used only in tests -prepareForTest' :: SolConf -> (VM, EventMap, NE.NonEmpty SolSignature, [Text], SignatureMap) +prepareForTest' :: SolConf -> (VM, EventMap, [SolSignature], [Text], SignatureMap) -> (VM, World, [EchidnaTest]) prepareForTest' SolConf{sender, testMode} (v, em, a, ts, _) = do let r = v._state._contract - a' = NE.toList a - (v, World sender M.empty Nothing [] em, createTests testMode True ts r a') + (v, World sender M.empty Nothing [] em, createTests testMode True ts r a) prepareHashMaps :: [FunctionHash] -> [FunctionHash] -> SignatureMap -> (SignatureMap, Maybe SignatureMap) prepareHashMaps [] _ m = (m, Nothing) -- No constant functions detected diff --git a/lib/Echidna/Types.hs b/lib/Echidna/Types.hs index 21c9bda61..c57725737 100644 --- a/lib/Echidna/Types.hs +++ b/lib/Echidna/Types.hs @@ -3,6 +3,7 @@ module Echidna.Types where import EVM (Error, EVM, VM) import Control.Exception (Exception) import Control.Monad.State.Strict (MonadState, runState, get, put) +import Data.Word (Word64) -- | We throw this when our execution fails due to something other than reversion. data ExecException = IllegalExec Error | UnknownFailure Error @@ -14,6 +15,7 @@ instance Show ExecException where instance Exception ExecException +type Gas = Word64 type MutationConsts a = (a, a, a, a) diff --git a/lib/Echidna/Types/Campaign.hs b/lib/Echidna/Types/Campaign.hs index 8e06ff616..2e9051b24 100644 --- a/lib/Echidna/Types/Campaign.hs +++ b/lib/Echidna/Types/Campaign.hs @@ -44,7 +44,7 @@ data Campaign = Campaign { _tests :: [EchidnaTest] -- ^ Tests being evaluated , _coverage :: CoverageMap -- ^ Coverage captured (NOTE: we don't always record this) - , _gasInfo :: Map Text (Int, [Tx]) + , _gasInfo :: Map Text (Gas, [Tx]) -- ^ Worst case gas (NOTE: we don't always record this) , _genDict :: GenDict -- ^ Generation dictionary diff --git a/lib/Echidna/UI/Report.hs b/lib/Echidna/UI/Report.hs index e25265418..aeba0b58d 100644 --- a/lib/Echidna/UI/Report.hs +++ b/lib/Echidna/UI/Report.hs @@ -10,6 +10,7 @@ import Data.Text qualified as T import Echidna.ABI (GenDict(..), encodeSig) import Echidna.Events (Events) import Echidna.Pretty (ppTxCall) +import Echidna.Types (Gas) import Echidna.Types.Campaign import Echidna.Types.Corpus (Corpus, corpusSize) import Echidna.Types.Coverage (CoverageMap, scoveragePoints) @@ -49,7 +50,7 @@ ppCorpus c | c == mempty = Nothing | otherwise = Just $ "Corpus size: " ++ show (corpusSize c) -- | Pretty-print the gas usage for a function. -ppGasOne :: MonadReader EConfig m => (Text, (Int, [Tx])) -> m String +ppGasOne :: MonadReader EConfig m => (Text, (Gas, [Tx])) -> m String ppGasOne ("", _) = pure "" ppGasOne (f, (g, xs)) = let pxs = mapM (ppTx $ length (nub $ (.src) <$> xs) /= 1) xs in (("\n" ++ unpack f ++ " used a maximum of " ++ show g ++ " gas\n Call sequence:\n") ++) . unlines . fmap (" " ++) <$> pxs diff --git a/src/test/Common.hs b/src/test/Common.hs index bfe446c22..ac5199a54 100644 --- a/src/test/Common.hs +++ b/src/test/Common.hs @@ -44,6 +44,7 @@ import Echidna.Config (parseConfig, defaultConfig) import Echidna.Campaign (campaign) import Echidna.Solidity (loadSolTests) import Echidna.Test (checkETest) +import Echidna.Types (Gas) import Echidna.Types.Config (Env(..), EConfig(..), EConfigWithUsage(..)) import Echidna.Types.Campaign (Campaign(..), CampaignConf(..)) import Echidna.Types.Signature (ContractName) @@ -183,10 +184,10 @@ solvedWith tx t = maybe False (any $ (== tx) . (.call)) . solnFor t solvedWithout :: TxCall -> Text -> Campaign -> Bool solvedWithout tx t = maybe False (all $ (/= tx) . (.call)) . solnFor t -getGas :: Text -> Campaign -> Maybe (Int, [Tx]) +getGas :: Text -> Campaign -> Maybe (Gas, [Tx]) getGas t camp = lookup t camp._gasInfo -gasInRange :: Text -> Int -> Int -> Campaign -> Bool +gasInRange :: Text -> Gas -> Gas -> Campaign -> Bool gasInRange t l h c = case getGas t c of Just (g, _) -> g >= l && g <= h _ -> False diff --git a/tests/solidity/values/extreme.yaml b/tests/solidity/values/extreme.yaml index 988696bf2..859a3da9d 100644 --- a/tests/solidity/values/extreme.yaml +++ b/tests/solidity/values/extreme.yaml @@ -2,3 +2,4 @@ testLimit: 5000 shrinkLimit: 100 testMode: assertion seqLen: 1 +seed: 1337