Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Read assert locations and determinate if they were executed or not #1110

Merged
merged 4 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions lib/Echidna.hs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ prepareContract cfg solFiles buildOutput selectedContract seed = do

let world = mkWorld cfg.solConf signatureMap selectedContract slitherInfo contracts

env <- mkEnv cfg buildOutput tests world
env <- mkEnv cfg buildOutput tests world (Just slitherInfo)

-- deploy contracts
vm <- loadSpecified env mainContract contracts
Expand Down Expand Up @@ -113,8 +113,8 @@ loadInitialCorpus env = do

pure $ persistedCorpus ++ ethenoCorpus

mkEnv :: EConfig -> BuildOutput -> [EchidnaTest] -> World -> IO Env
mkEnv cfg buildOutput tests world = do
mkEnv :: EConfig -> BuildOutput -> [EchidnaTest] -> World -> Maybe SlitherInfo -> IO Env
mkEnv cfg buildOutput tests world slitherInfo = do
codehashMap <- newIORef mempty
chainId <- maybe (pure Nothing) EVM.Fetch.fetchChainIdFrom cfg.rpcUrl
eventQueue <- newChan
Expand All @@ -128,4 +128,5 @@ mkEnv cfg buildOutput tests world = do
let dapp = dappInfo "/" buildOutput
pure $ Env { cfg, dapp, codehashMap, fetchContractCache, fetchSlotCache
, chainId, eventQueue, coverageRef, corpusRef, testRefs, world
, slitherInfo
}
31 changes: 30 additions & 1 deletion lib/Echidna/Output/Source.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -24,13 +25,14 @@ import System.Directory (createDirectoryIfMissing)
import System.FilePath ((</>))
import Text.Printf (printf)

import EVM.Dapp (srcMapCodePos)
import EVM.Dapp (srcMapCodePos, DappInfo(..))
import EVM.Solidity (SourceCache(..), SrcMap, SolcContract(..))

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 (AssertLocation(..), assertLocationList, SlitherInfo(..))

saveCoverages
:: Env
Expand Down Expand Up @@ -188,3 +190,30 @@ buildRuntimeLinesMap sc contracts =
where
srcMaps = concatMap
(\c -> toList $ c.runtimeSrcmap <> c.creationSrcmap) contracts

-- | Check that all assertions were hit, and log a warning if they weren't
checkAssertionsCoverage
:: SourceCache
-> Env
-> IO ()
checkAssertionsCoverage sc env = do
let
cs = Map.elems env.dapp.solcByName
asserts = maybe [] (concatMap assertLocationList . Map.elems . (.asserts)) env.slitherInfo
covMap <- readIORef env.coverageRef
covLines <- srcMapCov sc covMap cs
mapM_ (checkAssertionReached covLines) asserts

-- | Helper function for `checkAssertionsCoverage` which checks a single assertion
-- and logs a warning if it wasn't hit
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"
2 changes: 1 addition & 1 deletion lib/Echidna/Solidity.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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 []
samalws-tob marked this conversation as resolved.
Show resolved Hide resolved
as = if isAssertionMode testMode then filterResults maybeContract (assertFunctionList <$> 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
Expand Down
45 changes: 44 additions & 1 deletion lib/Echidna/SourceAnalysis/Slither.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

module Echidna.SourceAnalysis.Slither where

import Control.Applicative ((<|>))
import Data.Aeson ((.:), (.:?), (.!=), eitherDecode, parseJSON, withEmbeddedJSON, withObject)
import Data.Aeson.Types (FromJSON, Parser, Value(String))
import Data.ByteString.Base16 qualified as BS16 (decode)
Expand Down Expand Up @@ -40,11 +41,53 @@ 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)

-- | Assertion listing for a contract.
-- There are two possibilities because different solc's give different formats.
-- We either have a list of functions that have assertions, or a full listing of individual assertions.
data ContractAssertListing
= AssertFunctionList [FunctionName]
| AssertLocationList (Map FunctionName [AssertLocation])
deriving (Show)

type AssertListingByContract = Map ContractName ContractAssertListing

-- | Get a list of functions that have assertions
assertFunctionList :: ContractAssertListing -> [FunctionName]
assertFunctionList (AssertFunctionList l) = l
assertFunctionList (AssertLocationList m) = map fst $ filter (not . null . snd) $ Map.toList m

-- | Get a list of assertions, or an empty list if we don't have enough info
assertLocationList :: ContractAssertListing -> [AssertLocation]
assertLocationList (AssertFunctionList _) = []
assertLocationList (AssertLocationList m) = concat $ Map.elems m

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 {..}

instance FromJSON ContractAssertListing where
parseJSON x = (AssertFunctionList <$> parseJSON x) <|> (AssertLocationList <$> parseJSON x)

-- 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 :: AssertListingByContract
, constantValues :: Map ContractName (Map FunctionName [AbiValue])
, generationGraph :: Map ContractName (Map FunctionName [FunctionName])
, solcVersions :: [Version]
Expand Down
2 changes: 2 additions & 0 deletions lib/Echidna/Types/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Data.Word (Word64)
import EVM.Dapp (DappInfo)
import EVM.Types (Addr, Contract, W256)

import Echidna.SourceAnalysis.Slither (SlitherInfo)
import Echidna.SourceMapping (CodehashMap)
import Echidna.Types.Campaign (CampaignConf, CampaignEvent)
import Echidna.Types.Corpus (Corpus)
Expand Down Expand Up @@ -73,6 +74,7 @@ data Env = Env
, coverageRef :: IORef CoverageMap
, corpusRef :: IORef Corpus

, slitherInfo :: Maybe SlitherInfo
, codehashMap :: CodehashMap
, fetchContractCache :: IORef (Map Addr (Maybe Contract))
, fetchSlotCache :: IORef (Map Addr (Map W256 (Maybe W256)))
Expand Down
2 changes: 2 additions & 0 deletions src/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ main = withUtf8 $ withCP65001 $ do

tests <- traverse readIORef env.testRefs

checkAssertionsCoverage buildOutput.sources env

Onchain.saveRpcCache env

-- save corpus
Expand Down
2 changes: 1 addition & 1 deletion src/test/Common.hs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ loadSolTests cfg buildOutput name = do
world = World solConf.sender mempty Nothing [] eventMap
mainContract <- selectMainContract solConf name contracts
echidnaTests <- mkTests solConf mainContract
env <- mkEnv cfg buildOutput echidnaTests world
env <- mkEnv cfg buildOutput echidnaTests world Nothing
vm <- loadSpecified env mainContract contracts
pure (vm, env, echidnaTests)

Expand Down
Loading