Skip to content

Commit

Permalink
Simplify slither info parsing (#543)
Browse files Browse the repository at this point in the history
  • Loading branch information
arcz authored Feb 2, 2021
1 parent 683f737 commit a0509af
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 181 deletions.
16 changes: 7 additions & 9 deletions lib/Echidna.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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
Expand All @@ -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
244 changes: 93 additions & 151 deletions lib/Echidna/Processor.hs
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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' _ _ = []
26 changes: 18 additions & 8 deletions lib/Echidna/Solidity.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 =
Expand All @@ -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
Expand Down
Loading

0 comments on commit a0509af

Please sign in to comment.