Skip to content

Commit 778f63c

Browse files
Read assert locations and determinate if they were executed or not (#1110)
* Read assert locations and determinate if they were executed or not Co-authored-by: ggrieco-tob <[email protected]> * accomodate older versions of solc * cleanup * add SlitherInfo to Env --------- Co-authored-by: Sam Alws <[email protected]>
1 parent 6956030 commit 778f63c

File tree

7 files changed

+84
-7
lines changed

7 files changed

+84
-7
lines changed

lib/Echidna.hs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ prepareContract cfg solFiles buildOutput selectedContract seed = do
7070

7171
let world = mkWorld cfg.solConf signatureMap selectedContract slitherInfo contracts
7272

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

7575
-- deploy contracts
7676
vm <- loadSpecified env mainContract contracts
@@ -113,8 +113,8 @@ loadInitialCorpus env = do
113113

114114
pure $ persistedCorpus ++ ethenoCorpus
115115

116-
mkEnv :: EConfig -> BuildOutput -> [EchidnaTest] -> World -> IO Env
117-
mkEnv cfg buildOutput tests world = do
116+
mkEnv :: EConfig -> BuildOutput -> [EchidnaTest] -> World -> Maybe SlitherInfo -> IO Env
117+
mkEnv cfg buildOutput tests world slitherInfo = do
118118
codehashMap <- newIORef mempty
119119
chainId <- maybe (pure Nothing) EVM.Fetch.fetchChainIdFrom cfg.rpcUrl
120120
eventQueue <- newChan
@@ -128,4 +128,5 @@ mkEnv cfg buildOutput tests world = do
128128
let dapp = dappInfo "/" buildOutput
129129
pure $ Env { cfg, dapp, codehashMap, fetchContractCache, fetchSlotCache
130130
, chainId, eventQueue, coverageRef, corpusRef, testRefs, world
131+
, slitherInfo
131132
}

lib/Echidna/Output/Source.hs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module Echidna.Output.Source where
44

55
import Prelude hiding (writeFile)
66

7+
import Control.Monad (unless)
78
import Data.ByteString qualified as BS
89
import Data.Foldable
910
import Data.IORef (readIORef)
@@ -24,13 +25,14 @@ import System.Directory (createDirectoryIfMissing)
2425
import System.FilePath ((</>))
2526
import Text.Printf (printf)
2627

27-
import EVM.Dapp (srcMapCodePos)
28+
import EVM.Dapp (srcMapCodePos, DappInfo(..))
2829
import EVM.Solidity (SourceCache(..), SrcMap, SolcContract(..))
2930

3031
import Echidna.Types.Campaign (CampaignConf(..))
3132
import Echidna.Types.Config (Env(..), EConfig(..))
3233
import Echidna.Types.Coverage (OpIx, unpackTxResults, CoverageMap, CoverageFileType (..))
3334
import Echidna.Types.Tx (TxResult(..))
35+
import Echidna.SourceAnalysis.Slither (AssertLocation(..), assertLocationList, SlitherInfo(..))
3436

3537
saveCoverages
3638
:: Env
@@ -188,3 +190,30 @@ buildRuntimeLinesMap sc contracts =
188190
where
189191
srcMaps = concatMap
190192
(\c -> toList $ c.runtimeSrcmap <> c.creationSrcmap) contracts
193+
194+
-- | Check that all assertions were hit, and log a warning if they weren't
195+
checkAssertionsCoverage
196+
:: SourceCache
197+
-> Env
198+
-> IO ()
199+
checkAssertionsCoverage sc env = do
200+
let
201+
cs = Map.elems env.dapp.solcByName
202+
asserts = maybe [] (concatMap assertLocationList . Map.elems . (.asserts)) env.slitherInfo
203+
covMap <- readIORef env.coverageRef
204+
covLines <- srcMapCov sc covMap cs
205+
mapM_ (checkAssertionReached covLines) asserts
206+
207+
-- | Helper function for `checkAssertionsCoverage` which checks a single assertion
208+
-- and logs a warning if it wasn't hit
209+
checkAssertionReached :: Map String (Map Int [TxResult]) -> AssertLocation -> IO ()
210+
checkAssertionReached covLines assert =
211+
maybe
212+
warnAssertNotReached checkCoverage
213+
(Map.lookup assert.filenameAbsolute covLines)
214+
where
215+
checkCoverage coverage = let lineNumbers = Map.keys coverage in
216+
unless ((head assert.assertLines) `elem` lineNumbers) warnAssertNotReached
217+
warnAssertNotReached =
218+
putStrLn $ "WARNING: assertion at file: " ++ assert.filenameRelative
219+
++ " starting at line: " ++ show (head assert.assertLines) ++ " was never reached"

lib/Echidna/Solidity.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ mkWorld SolConf{sender, testMode} sigMap maybeContract slitherInfo contracts =
342342
let
343343
eventMap = Map.unions $ map (.eventMap) contracts
344344
payableSigs = filterResults maybeContract slitherInfo.payableFunctions
345-
as = if isAssertionMode testMode then filterResults maybeContract slitherInfo.asserts else []
345+
as = if isAssertionMode testMode then filterResults maybeContract (assertFunctionList <$> slitherInfo.asserts) else []
346346
cs = if isDapptestMode testMode then [] else filterResults maybeContract slitherInfo.constantFunctions \\ as
347347
(highSignatureMap, lowSignatureMap) = prepareHashMaps cs as $
348348
filterFallbacks slitherInfo.fallbackDefined slitherInfo.receiveDefined contracts sigMap

lib/Echidna/SourceAnalysis/Slither.hs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
module Echidna.SourceAnalysis.Slither where
44

5+
import Control.Applicative ((<|>))
56
import Data.Aeson ((.:), (.:?), (.!=), eitherDecode, parseJSON, withEmbeddedJSON, withObject)
67
import Data.Aeson.Types (FromJSON, Parser, Value(String))
78
import Data.ByteString.Base16 qualified as BS16 (decode)
@@ -40,11 +41,53 @@ enhanceConstants si =
4041
enh (AbiString s) = makeArrayAbiValues s
4142
enh v = [v]
4243

44+
data AssertLocation = AssertLocation
45+
{ start :: Int
46+
, filenameRelative :: String
47+
, filenameAbsolute :: String
48+
, assertLines :: [Int]
49+
, startColumn :: Int
50+
, endingColumn :: Int
51+
} deriving (Show)
52+
53+
-- | Assertion listing for a contract.
54+
-- There are two possibilities because different solc's give different formats.
55+
-- We either have a list of functions that have assertions, or a full listing of individual assertions.
56+
data ContractAssertListing
57+
= AssertFunctionList [FunctionName]
58+
| AssertLocationList (Map FunctionName [AssertLocation])
59+
deriving (Show)
60+
61+
type AssertListingByContract = Map ContractName ContractAssertListing
62+
63+
-- | Get a list of functions that have assertions
64+
assertFunctionList :: ContractAssertListing -> [FunctionName]
65+
assertFunctionList (AssertFunctionList l) = l
66+
assertFunctionList (AssertLocationList m) = map fst $ filter (not . null . snd) $ Map.toList m
67+
68+
-- | Get a list of assertions, or an empty list if we don't have enough info
69+
assertLocationList :: ContractAssertListing -> [AssertLocation]
70+
assertLocationList (AssertFunctionList _) = []
71+
assertLocationList (AssertLocationList m) = concat $ Map.elems m
72+
73+
instance FromJSON AssertLocation where
74+
parseJSON = withObject "" $ \o -> do
75+
start <- o.: "start"
76+
filenameRelative <- o.: "filename_relative"
77+
filenameAbsolute <- o.: "filename_absolute"
78+
assertLines <- o.: "lines"
79+
startColumn <- o.: "starting_column"
80+
endingColumn <- o.: "ending_column"
81+
pure AssertLocation {..}
82+
83+
instance FromJSON ContractAssertListing where
84+
parseJSON x = (AssertFunctionList <$> parseJSON x) <|> (AssertLocationList <$> parseJSON x)
85+
4386
-- we loose info on what constants are in which functions
4487
data SlitherInfo = SlitherInfo
4588
{ payableFunctions :: Map ContractName [FunctionName]
4689
, constantFunctions :: Map ContractName [FunctionName]
47-
, asserts :: Map ContractName [FunctionName]
90+
, asserts :: AssertListingByContract
4891
, constantValues :: Map ContractName (Map FunctionName [AbiValue])
4992
, generationGraph :: Map ContractName (Map FunctionName [FunctionName])
5093
, solcVersions :: [Version]

lib/Echidna/Types/Config.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import Data.Word (Word64)
1212
import EVM.Dapp (DappInfo)
1313
import EVM.Types (Addr, Contract, W256)
1414

15+
import Echidna.SourceAnalysis.Slither (SlitherInfo)
1516
import Echidna.SourceMapping (CodehashMap)
1617
import Echidna.Types.Campaign (CampaignConf, CampaignEvent)
1718
import Echidna.Types.Corpus (Corpus)
@@ -73,6 +74,7 @@ data Env = Env
7374
, coverageRef :: IORef CoverageMap
7475
, corpusRef :: IORef Corpus
7576

77+
, slitherInfo :: Maybe SlitherInfo
7678
, codehashMap :: CodehashMap
7779
, fetchContractCache :: IORef (Map Addr (Maybe Contract))
7880
, fetchSlotCache :: IORef (Map Addr (Map W256 (Maybe W256)))

src/Main.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ main = withUtf8 $ withCP65001 $ do
7070

7171
tests <- traverse readIORef env.testRefs
7272

73+
checkAssertionsCoverage buildOutput.sources env
74+
7375
Onchain.saveRpcCache env
7476

7577
-- save corpus

src/test/Common.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ loadSolTests cfg buildOutput name = do
157157
world = World solConf.sender mempty Nothing [] eventMap
158158
mainContract <- selectMainContract solConf name contracts
159159
echidnaTests <- mkTests solConf mainContract
160-
env <- mkEnv cfg buildOutput echidnaTests world
160+
env <- mkEnv cfg buildOutput echidnaTests world Nothing
161161
vm <- loadSpecified env mainContract contracts
162162
pure (vm, env, echidnaTests)
163163

0 commit comments

Comments
 (0)