From 8d84feb011ee89f0fac5e061b4be3108a5cb153d Mon Sep 17 00:00:00 2001 From: Sam Alws Date: Tue, 20 Aug 2024 15:38:55 -0400 Subject: [PATCH] Read assert locations and determinate if they were executed or not Co-authored-by: ggrieco-tob --- lib/Echidna.hs | 4 ++-- lib/Echidna/Output/Source.hs | 25 +++++++++++++++++++++++++ lib/Echidna/Solidity.hs | 4 ++-- lib/Echidna/SourceAnalysis/Slither.hs | 23 ++++++++++++++++++++++- src/Main.hs | 7 +++++-- 5 files changed, 56 insertions(+), 7 deletions(-) diff --git a/lib/Echidna.hs b/lib/Echidna.hs index 64b84fe93..ab608fb5d 100644 --- a/lib/Echidna.hs +++ b/lib/Echidna.hs @@ -51,7 +51,7 @@ prepareContract -> BuildOutput -> Maybe ContractName -> Seed - -> IO (VM Concrete RealWorld, Env, GenDict) + -> IO (VM Concrete RealWorld, Env, GenDict, AssertMappingByContract) prepareContract cfg solFiles buildOutput selectedContract seed = do let solConf = cfg.solConf (Contracts contractMap) = buildOutput.contracts @@ -90,7 +90,7 @@ prepareContract cfg solFiles buildOutput selectedContract seed = do seed (returnTypes contracts) - pure (vm, env, dict) + pure (vm, env, dict, slitherInfo.asserts) loadInitialCorpus :: Env -> IO [(FilePath, [Tx])] loadInitialCorpus env = do diff --git a/lib/Echidna/Output/Source.hs b/lib/Echidna/Output/Source.hs index c6c3ab98f..45393c445 100644 --- a/lib/Echidna/Output/Source.hs +++ b/lib/Echidna/Output/Source.hs @@ -4,6 +4,7 @@ module Echidna.Output.Source where import Prelude hiding (writeFile) +import Control.Monad (unless) import Data.ByteString qualified as BS import Data.Foldable import Data.IORef (readIORef) @@ -31,6 +32,7 @@ import Echidna.Types.Campaign (CampaignConf(..)) import Echidna.Types.Config (Env(..), EConfig(..)) import Echidna.Types.Coverage (OpIx, unpackTxResults, CoverageMap, CoverageFileType (..)) import Echidna.Types.Tx (TxResult(..)) +import Echidna.SourceAnalysis.Slither (AssertMappingByContract, AssertLocation(..)) saveCoverages :: Env @@ -188,3 +190,26 @@ buildRuntimeLinesMap sc contracts = where srcMaps = concatMap (\c -> toList $ c.runtimeSrcmap <> c.creationSrcmap) contracts + +checkAssertionsCoverage + :: SourceCache + -> [SolcContract] + -> CoverageMap + -> AssertMappingByContract + -> IO () +checkAssertionsCoverage sc cs covMap assertMap = do + covLines <- srcMapCov sc covMap cs + let asserts = concat $ concatMap Map.elems $ Map.elems assertMap + mapM_ (checkAssertionReached covLines) asserts + +checkAssertionReached :: Map String (Map Int [TxResult]) -> AssertLocation -> IO () +checkAssertionReached covLines assert = + maybe + warnAssertNotReached checkCoverage + (Map.lookup assert.filenameAbsolute covLines) + where + checkCoverage coverage = let lineNumbers = Map.keys coverage in + unless ((head assert.assertLines) `elem` lineNumbers) warnAssertNotReached + warnAssertNotReached = + putStrLn $ "WARNING: assertion at file: " ++ assert.filenameRelative + ++ " starting at line: " ++ show (head assert.assertLines) ++ " was never reached" diff --git a/lib/Echidna/Solidity.hs b/lib/Echidna/Solidity.hs index b7e599e16..d298b5b8b 100644 --- a/lib/Echidna/Solidity.hs +++ b/lib/Echidna/Solidity.hs @@ -41,7 +41,7 @@ import Echidna.Etheno (loadEthenoBatch) import Echidna.Events (extractEvents) import Echidna.Exec (execTx, initialVM) import Echidna.SourceAnalysis.Slither -import Echidna.Test (createTests, isAssertionMode, isPropertyMode, isDapptestMode) +import Echidna.Test (createTests, isPropertyMode, isDapptestMode) import Echidna.Types.Config (EConfig(..), Env(..)) import Echidna.Types.Signature (ContractName, SolSignature, SignatureMap, FunctionName) @@ -342,7 +342,7 @@ mkWorld SolConf{sender, testMode} sigMap maybeContract slitherInfo contracts = let eventMap = Map.unions $ map (.eventMap) contracts payableSigs = filterResults maybeContract slitherInfo.payableFunctions - as = if isAssertionMode testMode then filterResults maybeContract slitherInfo.asserts else [] + as = {- if isAssertionMode testMode then filterResults maybeContract slitherInfo.asserts else -} [] cs = if isDapptestMode testMode then [] else filterResults maybeContract slitherInfo.constantFunctions \\ as (highSignatureMap, lowSignatureMap) = prepareHashMaps cs as $ filterFallbacks slitherInfo.fallbackDefined slitherInfo.receiveDefined contracts sigMap diff --git a/lib/Echidna/SourceAnalysis/Slither.hs b/lib/Echidna/SourceAnalysis/Slither.hs index cb60028bb..490578cf2 100644 --- a/lib/Echidna/SourceAnalysis/Slither.hs +++ b/lib/Echidna/SourceAnalysis/Slither.hs @@ -40,11 +40,32 @@ enhanceConstants si = enh (AbiString s) = makeArrayAbiValues s enh v = [v] +data AssertLocation = AssertLocation + { start :: Int + , filenameRelative :: String + , filenameAbsolute :: String + , assertLines :: [Int] + , startColumn :: Int + , endingColumn :: Int + } deriving (Show) + +type AssertMappingByContract = Map ContractName (Map FunctionName [AssertLocation]) + +instance FromJSON AssertLocation where + parseJSON = withObject "" $ \o -> do + start <- o.: "start" + filenameRelative <- o.: "filename_relative" + filenameAbsolute <- o.: "filename_absolute" + assertLines <- o.: "lines" + startColumn <- o.: "starting_column" + endingColumn <- o.: "ending_column" + pure AssertLocation {..} + -- we loose info on what constants are in which functions data SlitherInfo = SlitherInfo { payableFunctions :: Map ContractName [FunctionName] , constantFunctions :: Map ContractName [FunctionName] - , asserts :: Map ContractName [FunctionName] + , asserts :: AssertMappingByContract , constantValues :: Map ContractName (Map FunctionName [AbiValue]) , generationGraph :: Map ContractName (Map FunctionName [FunctionName]) , solcVersions :: [Version] diff --git a/src/Main.hs b/src/Main.hs index 9e1d68d49..1750a420d 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -62,7 +62,7 @@ main = withUtf8 $ withCP65001 $ do -- take the seed from config, otherwise generate a new one seed <- maybe (getRandomR (0, maxBound)) pure cfg.campaignConf.seed - (vm, env, dict) <- prepareContract cfg cliFilePath buildOutput cliSelectedContract seed + (vm, env, dict, asserts) <- prepareContract cfg cliFilePath buildOutput cliSelectedContract seed initialCorpus <- loadInitialCorpus env -- start ui and run tests @@ -70,6 +70,10 @@ main = withUtf8 $ withCP65001 $ do tests <- traverse readIORef env.testRefs + let contracts = Map.elems env.dapp.solcByName + coverage <- readIORef env.coverageRef + checkAssertionsCoverage buildOutput.sources contracts coverage asserts + Onchain.saveRpcCache env -- save corpus @@ -108,7 +112,6 @@ main = withUtf8 $ withCP65001 $ do Onchain.saveCoverageReport env runId -- save source coverage reports - let contracts = Map.elems env.dapp.solcByName saveCoverages env runId dir buildOutput.sources contracts if isSuccessful tests then exitSuccess else exitWith (ExitFailure 1)