diff --git a/lib/Echidna.hs b/lib/Echidna.hs index 4ca4f6f50..3a104ce21 100644 --- a/lib/Echidna.hs +++ b/lib/Echidna.hs @@ -5,10 +5,9 @@ module Echidna where import Control.Lens (view, (^.), to) import Data.Has (Has(..)) import Control.Monad.Catch (MonadCatch(..)) -import Control.Monad.Reader (MonadReader, MonadIO, liftIO, when) +import Control.Monad.Reader (MonadReader, MonadIO, liftIO) import Control.Monad.Random (MonadRandom) import Data.Map.Strict (keys) -import Data.Text (pack) import EVM (env, contracts, VM) import EVM.ABI (AbiValue(AbiAddress)) @@ -18,6 +17,7 @@ import Echidna.Config import Echidna.Solidity import Echidna.Types.Campaign import Echidna.Types.Random +import Echidna.Types.Signature import Echidna.Types.Tx import Echidna.Types.World import Echidna.Transaction @@ -39,27 +39,25 @@ import qualified Data.List.NonEmpty as NE -- * A list of transaction sequences to initialize the corpus prepareContract :: (MonadCatch m, MonadRandom m, MonadReader x m, MonadIO m, MonadFail m, Has TxConf x, Has SolConf x) - => EConfig -> NE.NonEmpty FilePath -> Maybe String -> Seed -> m (VM, World, [SolTest], Maybe GenDict, [[Tx]]) + => EConfig -> NE.NonEmpty FilePath -> Maybe ContractName -> Seed -> m (VM, World, [SolTest], Maybe GenDict, [[Tx]]) prepareContract cfg fs c g = do txs <- liftIO $ loadTxs cd -- compile and load contracts cs <- Echidna.Solidity.contracts fs ads <- addresses - p <- loadSpecified (pack <$> c) cs + p <- loadSpecified c cs -- run processors ca <- view (hasLens . cryticArgs) si <- runSlither (NE.head fs) ca - when (null si) $ liftIO $ putStrLn "WARNING: slither failed to run or extracted no information at all" - - -- filter extracted constants - let extractedConstants = filterConstantValue si -- load tests (v, w, ts) <- prepareForTest p c si let ads' = AbiAddress <$> v ^. env . EVM.contracts . to keys + let constants' = enhanceConstants si ++ timeConstants ++ largeConstants ++ NE.toList ads ++ ads' + -- start ui and run tests - return (v, w, ts, Just $ mkGenDict df (extractedConstants ++ timeConstants ++ largeConstants ++ NE.toList ads ++ ads') [] g (returnTypes cs), txs) + return (v, w, ts, Just $ mkGenDict df constants' [] g (returnTypes cs), txs) where cd = cfg ^. cConf . corpusDir df = cfg ^. cConf . dictFreq diff --git a/lib/Echidna/Processor.hs b/lib/Echidna/Processor.hs index fb039d1d8..6605a3c39 100644 --- a/lib/Echidna/Processor.hs +++ b/lib/Echidna/Processor.hs @@ -1,25 +1,25 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE RankNTypes #-} -{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} module Echidna.Processor where -import Control.Arrow (second) -import Control.Lens -import Control.Monad.IO.Class (MonadIO(..)) -import Control.Exception (Exception) -import Control.Monad.Catch (MonadThrow(..)) -import Data.Aeson (decode, Value(..)) -import Data.Text (Text, pack, unpack) -import Data.List (nub) -import Data.Maybe (maybeToList) -import Text.Read (readMaybe) -import System.Directory (findExecutable) -import System.Process (StdStream(..), readCreateProcessWithExitCode, proc, std_err) -import System.Exit (ExitCode(..)) +import Control.Monad.IO.Class (MonadIO(..)) +import Control.Exception (Exception) +import Control.Monad.Catch (MonadThrow(..)) +import Data.Aeson ((.:), decode, parseJSON, withEmbeddedJSON, withObject) +import Data.Aeson.Types (FromJSON, Parser, Value(String)) +import Data.List (nub) +import Data.Maybe (catMaybes) +import Text.Read (readMaybe) +import System.Directory (findExecutable) +import System.Process (StdStream(..), readCreateProcessWithExitCode, proc, std_err) +import System.Exit (ExitCode(..)) import qualified Data.ByteString.Lazy.Char8 as BSL import qualified Data.ByteString.UTF8 as BSU +import qualified Data.List.NonEmpty as NE import qualified Data.HashMap.Strict as M import Echidna.Types.Signature (ContractName, FunctionName, FunctionHash) @@ -42,151 +42,93 @@ instance Exception ProcException -- | This function is used to filter the lists of function names according to the supplied -- contract name (if any) and returns a list of hashes -filterResults :: Maybe String -> [(ContractName, [FunctionName])] -> [FunctionHash] +filterResults :: Maybe ContractName -> M.HashMap ContractName [FunctionName] -> [FunctionHash] filterResults (Just c) rs = - case lookup (pack c) rs of + case M.lookup c rs of Nothing -> filterResults Nothing rs Just s -> hashSig <$> s - -filterResults Nothing rs = concatMap (fmap hashSig . snd) rs - -data SlitherInfo = PayableInfo (ContractName, [FunctionName]) - | ConstantFunctionInfo (ContractName, [FunctionName]) - | AssertFunction (ContractName, [FunctionName]) - | ConstantValue AbiValue - | GenerationGraph (ContractName, FunctionName, [FunctionName]) - deriving (Show) -makePrisms ''SlitherInfo - -slitherFilter :: Prism' SlitherInfo a -> [SlitherInfo] -> [a] -slitherFilter p = toListOf (traverse . p) - -filterPayable :: [SlitherInfo] -> [(ContractName, [FunctionName])] -filterPayable = slitherFilter _PayableInfo - -filterAssert :: [SlitherInfo] -> [(ContractName, [FunctionName])] -filterAssert = slitherFilter _AssertFunction - -filterConstantFunction :: [SlitherInfo] -> [(ContractName, [FunctionName])] -filterConstantFunction = slitherFilter _ConstantFunctionInfo - -filterGenerationGraph :: [SlitherInfo] -> [(ContractName, FunctionName, [FunctionName])] -filterGenerationGraph = slitherFilter _GenerationGraph - -filterConstantValue :: [SlitherInfo] -> [AbiValue] -filterConstantValue = slitherFilter _ConstantValue +filterResults Nothing rs = hashSig <$> (concat . M.elems) rs + +enhanceConstants :: SlitherInfo -> [AbiValue] +enhanceConstants si = + nub . concatMap enh . concat . concat . M.elems $ M.elems <$> constantValues si + where + enh (AbiUInt _ n) = makeNumAbiValues (fromIntegral n) + enh (AbiInt _ n) = makeNumAbiValues (fromIntegral n) + enh (AbiString s) = makeArrayAbiValues s + enh v = [v] + +-- we loose info on what constants are in which functions +data SlitherInfo = SlitherInfo + { payableFunctions :: M.HashMap ContractName [FunctionName] + , constantFunctions :: M.HashMap ContractName [FunctionName] + , asserts :: M.HashMap ContractName [FunctionName] + , constantValues :: M.HashMap ContractName (M.HashMap FunctionName [AbiValue]) + , generationGraph :: M.HashMap ContractName (M.HashMap FunctionName [FunctionName]) + } deriving (Show) + +instance FromJSON SlitherInfo where + parseJSON = withObject "slitherOutput" $ \o -> do + -- take the value under 'description' through the path - $['results']['printers'][0]['description'] + results <- o .: "results" + printer <- NE.head <$> results .: "printers" -- there must be at least one printer + description <- printer .: "description" + -- description is a JSON string, needs additional parsing + withEmbeddedJSON "descriptionString" parseDescription (String description) + where + parseDescription = withObject "description" $ \o -> do + payableFunctions <- o .: "payable" + constantFunctions <- o .: "constant_functions" + asserts <- o .: "assert" + constantValues' + -- the type annotation is needed + :: M.HashMap ContractName (M.HashMap FunctionName [[Maybe AbiValue]]) + <- o .: "constants_used" >>= (traverse . traverse . traverse . traverse) parseConstant + -- flatten [[AbiValue]], the array probably shouldn't be nested, fix it in Slither + let constantValues = (fmap . fmap) (catMaybes . concat) constantValues' + functionsRelations <- o .: "functions_relations" + generationGraph <- (traverse . traverse) (withObject "relations" (.: "impacts")) functionsRelations + pure SlitherInfo {..} + + parseConstant :: Value -> Parser (Maybe AbiValue) + parseConstant = withObject "const" $ \o -> do + 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 + + "string" -> + pure . Just . AbiString $ BSU.fromString v + + "address" -> + case AbiAddress . Addr <$> readMaybe v of + Nothing -> failure v t + a -> pure a + + -- we don't need all the types for now + _ -> pure Nothing + where failure v t = fail $ "failed to parse " ++ t ++ ": " ++ v -- Slither processing -runSlither :: (MonadIO m, MonadThrow m) => FilePath -> [String] -> m [SlitherInfo] -runSlither fp args = let args' = ["--ignore-compile", "--print", "echidna", "--json", "-"] ++ args in do +runSlither :: (MonadIO m, MonadThrow m) => FilePath -> [String] -> m SlitherInfo +runSlither fp extraArgs = do mp <- liftIO $ findExecutable "slither" case mp of Nothing -> throwM $ ProcessorNotFound "slither" "You should install it using 'pip3 install slither-analyzer --user'" Just path -> liftIO $ do - (ec, out, err) <- readCreateProcessWithExitCode (proc path $ args' |> fp) {std_err = Inherit} "" + let args = ["--ignore-compile", "--print", "echidna", "--json", "-"] ++ extraArgs ++ [fp] + (ec, out, err) <- readCreateProcessWithExitCode (proc path args) {std_err = Inherit} "" case ec of - ExitSuccess -> return $ procSlither out + ExitSuccess -> + case decode (BSL.pack out) of + Just si -> pure si + Nothing -> throwM $ ProcessorFailure "slither" "decoding slither output failed" ExitFailure _ -> throwM $ ProcessorFailure "slither" err - -procSlither :: String -> [SlitherInfo] -procSlither r = - case (decode . BSL.pack) r of - Nothing -> [] - Just v -> mresult "" v - --- parse result json -mresult :: Text -> Value -> [SlitherInfo] -mresult "description" (String x) = - case (decode . BSL.pack . unpack) x of - Nothing -> [] - Just v -> mpayable "" v ++ mcfuncs "" v ++ mggraph "" v ++ mconsts "" v ++ massert "" v - -mresult _ (Object o) = concatMap (uncurry mresult) $ M.toList o -mresult _ (Array a) = concatMap (mresult "") a -mresult _ _ = [] - --- parse actual payable information -mpayable :: Text -> Value -> [SlitherInfo] -mpayable "payable" (Object o) = PayableInfo . second f <$> M.toList o - where f (Array xs) = concatMap f xs - f (String "fallback()") = ["()"] - f (String s) = [s] - f _ = [] - -mpayable _ (Object o) = concatMap (uncurry mpayable) $ M.toList o -mpayable _ (Array a) = concatMap (mpayable "") a -mpayable _ _ = [] - --- parse actual assert information -massert :: Text -> Value -> [SlitherInfo] -massert "assert" (Object o) = AssertFunction . second f <$> M.toList o - where f (Array xs) = concatMap f xs - f (String "fallback()") = ["()"] - f (String s) = [s] - f _ = [] - -massert _ (Object o) = concatMap (uncurry massert) $ M.toList o -massert _ (Array a) = concatMap (massert "") a -massert _ _ = [] - --- parse actual constant functions information -mcfuncs :: Text -> Value -> [SlitherInfo] -mcfuncs "constant_functions" (Object o) = ConstantFunctionInfo . second f <$> M.toList o - where f (Array xs) = concatMap f xs - f (String s) = [s] - f _ = [] - -mcfuncs _ (Object o) = concatMap (uncurry mcfuncs) $ M.toList o -mcfuncs _ (Array a) = concatMap (mcfuncs "") a -mcfuncs _ _ = [] - --- parse actual constant functions information -mconsts :: Text -> Value -> [SlitherInfo] -mconsts "constants_used" (Object o) = concatMap (uncurry mconsts') $ M.toList o -mconsts _ (Object o) = concatMap (uncurry mconsts) $ M.toList o -mconsts _ (Array a) = concatMap (mconsts "") a -mconsts _ _ = [] - -mconsts' :: Text -> Value -> [SlitherInfo] -mconsts' _ (Object o) = case (M.lookup "value" o, M.lookup "type" o) of - (Just (String s), Just (String t)) -> map ConstantValue $ nub $ parseAbiValue (unpack s, unpack t) - (Nothing, Nothing) -> concatMap (uncurry mconsts') $ M.toList o - _ -> error "invalid JSON formatting parsing constants" - -mconsts' _ (Array a) = concatMap (mconsts' "") a -mconsts' _ _ = [] - -parseAbiValue :: (String, String) -> [AbiValue] -parseAbiValue (v, 'u':'i':'n':'t':_) = case readMaybe v of - Just m -> makeNumAbiValues m - _ -> [] - -parseAbiValue (v, 'i':'n':'t':_) = case readMaybe v of - Just m -> makeNumAbiValues m - _ -> [] - -parseAbiValue (v, "string") = makeArrayAbiValues $ BSU.fromString v -parseAbiValue (v, "address") = maybeToList $ AbiAddress . Addr <$> readMaybe v -parseAbiValue _ = [] - - --- parse actual generation graph -mggraph :: Text -> Value -> [SlitherInfo] -mggraph "functions_relations" (Object o) = concatMap f $ M.toList o - where f (c, Object o1) = map (\(m,v) -> GenerationGraph (c, m, mggraph' "" v)) $ M.toList o1 - f _ = [] - -mggraph _ (Object o) = concatMap (uncurry mggraph) $ M.toList o -mggraph _ (Array a) = concatMap (mggraph "") a -mggraph _ _ = [] - -mggraph' :: Text -> Value -> [Text] -mggraph' "impacts" (Array a) = concatMap f a - where f (Array xs) = concatMap f xs - f (String "fallback()") = ["()"] - f (String s) = [s] - f _ = [] - -mggraph' _ (Object o) = concatMap (uncurry mggraph') $ M.toList o -mggraph' _ (Array a) = concatMap (mggraph' "") a -mggraph' _ _ = [] diff --git a/lib/Echidna/Solidity.hs b/lib/Echidna/Solidity.hs index 84172fc56..336ae4a49 100644 --- a/lib/Echidna/Solidity.hs +++ b/lib/Echidna/Solidity.hs @@ -30,7 +30,7 @@ import Echidna.ABI (encodeSig, encodeSigWithName, hashSig, fallba import Echidna.Exec (execTx, initialVM) import Echidna.Events (EventMap) import Echidna.RPC (loadEthenoBatch) -import Echidna.Types.Signature (FunctionHash, SolSignature, SignatureMap, getBytecodeMetadata) +import Echidna.Types.Signature (ContractName, FunctionHash, SolSignature, SignatureMap, getBytecodeMetadata) import Echidna.Types.Tx (TxConf, createTx, createTxWithValue, unlimitedGasPerBlock, initialTimestamp, initialBlockNumber) import Echidna.Types.World (World(..)) import Echidna.Processor @@ -207,7 +207,7 @@ loadSpecified name cs = do let fabiOfc = filterMethods (c ^. contractName) fs $ abiOf pref c -- Filter again for 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 -> (cc ^. runtimeCode . to getBytecodeMetadata, filterMethods (cc ^. contractName) fs $ abiOf pref cc) else M.singleton (c ^. runtimeCode . to getBytecodeMetadata) fabiOfc @@ -259,21 +259,31 @@ loadWithCryticCompile fp name = contracts fp >>= loadSpecified name -- for running a 'Campaign' against the tests found. prepareForTest :: (MonadReader x m, Has SolConf x) => (VM, EventMap, NE.NonEmpty SolSignature, [Text], SignatureMap) - -> Maybe String - -> [SlitherInfo] + -> Maybe ContractName + -> SlitherInfo -> m (VM, World, [SolTest]) prepareForTest (v, em, a, ts, m) c si = do SolConf{ _sender = s, _checkAsserts = ch } <- view hasLens let r = v ^. state . contract a' = NE.toList a - ps = filterResults c $ filterPayable si - as = if ch then filterResults c $ filterAssert si else [] - cs = filterResults c $ filterConstantFunction si + ps = filterResults c $ payableFunctions si + as = if ch then filterResults c $ asserts si else [] + cs = filterResults c $ constantFunctions si (hm, lm) = prepareHashMaps cs as m pure (v, World s hm lm ps em, fmap Left (zip ts $ repeat r) ++ if ch then Right <$> drop 1 a' else []) +-- this limited variant is used only in tests +prepareForTest' :: (MonadReader x m, Has SolConf x) + => (VM, EventMap, NE.NonEmpty SolSignature, [Text], SignatureMap) + -> m (VM, World, [SolTest]) +prepareForTest' (v, em, a, ts, _) = do + SolConf{ _sender = s, _checkAsserts = ch } <- view hasLens + let r = v ^. state . contract + a' = NE.toList a + pure (v, World s M.empty Nothing [] em, fmap Left (zip ts $ repeat r) ++ if ch then Right <$> drop 1 a' else []) + prepareHashMaps :: [FunctionHash] -> [FunctionHash] -> SignatureMap -> (SignatureMap, Maybe SignatureMap) prepareHashMaps [] _ m = (m, Nothing) -- No constant functions detected prepareHashMaps cs as m = @@ -288,7 +298,7 @@ prepareHashMaps cs as m = -- a testing function. loadSolTests :: (MonadIO m, MonadThrow m, MonadReader x m, Has SolConf x, Has TxConf x, MonadFail m) => NE.NonEmpty FilePath -> Maybe Text -> m (VM, World, [SolTest]) -loadSolTests fp name = loadWithCryticCompile fp name >>= (\t -> prepareForTest t Nothing []) +loadSolTests fp name = loadWithCryticCompile fp name >>= prepareForTest' mkLargeAbiInt :: Int -> AbiValue mkLargeAbiInt i = AbiInt i $ 2 ^ (i - 1) - 1 diff --git a/src/Main.hs b/src/Main.hs index 7eb7adf20..ac2df4198 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -4,7 +4,7 @@ import Control.Lens ((^.), (.~), (&)) import Control.Monad (unless) import Control.Monad.Reader (runReaderT) import Control.Monad.Random (getRandom) -import Data.Text (unpack) +import Data.Text (pack, unpack) import Data.Version (showVersion) import Options.Applicative import Paths_echidna (version) @@ -63,7 +63,7 @@ main = do let cd = cfg ^. cConf . corpusDir cpg <- flip runReaderT cfg $ do - (v, w, ts, d, txs) <- prepareContract cfg f c g + (v, w, ts, d, txs) <- prepareContract cfg f (pack <$> c) g -- start ui and run tests ui v w ts d txs diff --git a/src/test/Common.hs b/src/test/Common.hs index d0406feb2..6d974c139 100644 --- a/src/test/Common.hs +++ b/src/test/Common.hs @@ -11,7 +11,7 @@ module Common , passed , solvedLen , solvedWith - , solvedWithout + , solvedWithout , getGas , gasInRange , countCorpus @@ -44,6 +44,7 @@ import Echidna.Config (EConfig, _econfig, parseConfig, defaultConfig, sConf, cCo import Echidna.Solidity (loadSolTests, quiet) import Echidna.Test (checkETest) import Echidna.Types.Campaign (Campaign, TestState(..), testLimit, shrinkLimit, tests, gasInfo, corpus, coverage) +import Echidna.Types.Signature (ContractName) import Echidna.Types.Tx (Tx(..), TxCall(..), call) import Echidna.Types.World (eventMap) @@ -58,19 +59,17 @@ type SolcVersionComp = Version -> Bool solcV :: (Int, Int, Int) -> SolcVersion solcV (x,y,z) = version x y z [] [] -withSolcVersion :: Maybe SolcVersionComp -> IO () -> IO () -withSolcVersion Nothing t = t -withSolcVersion (Just f) t = do +withSolcVersion :: Maybe SolcVersionComp -> IO () -> IO () +withSolcVersion Nothing t = t +withSolcVersion (Just f) t = do sv <- readProcess "solc" ["--version"] "" let (_:sv':_) = splitOn "Version: " sv let (sv'':_) = splitOn "+" sv' - case fromText $ pack sv'' of - Right v' -> if f v' then t else assertBool "skip" True + case fromText $ pack sv'' of + Right v' -> if f v' then t else assertBool "skip" True Left e -> error $ show e -type Name = String - -runContract :: FilePath -> Maybe String -> EConfig -> IO Campaign +runContract :: FilePath -> Maybe ContractName -> EConfig -> IO Campaign runContract f c cfg = flip runReaderT cfg $ do g <- getRandom @@ -84,8 +83,8 @@ testContract fp cfg = testContract' fp Nothing Nothing cfg True testContractV :: FilePath -> Maybe SolcVersionComp -> Maybe FilePath -> [(String, Campaign -> Bool)] -> TestTree testContractV fp v cfg = testContract' fp Nothing v cfg True -testContract' :: FilePath -> Maybe Name -> Maybe SolcVersionComp -> Maybe FilePath -> Bool -> [(String, Campaign -> Bool)] -> TestTree -testContract' fp n v cfg s as = testCase fp $ withSolcVersion v $ do +testContract' :: FilePath -> Maybe ContractName -> Maybe SolcVersionComp -> Maybe FilePath -> Bool -> [(String, Campaign -> Bool)] -> TestTree +testContract' fp n v cfg s as = testCase fp $ withSolcVersion v $ do c <- set (sConf . quiet) True <$> maybe (pure testConfig) (fmap _econfig . parseConfig) cfg let c' = c & sConf . quiet .~ True & (if s then cConf . testLimit .~ 10000 else id)