diff --git a/lib/Echidna.hs b/lib/Echidna.hs index 9db925668..4dcd4bb6b 100644 --- a/lib/Echidna.hs +++ b/lib/Echidna.hs @@ -1,7 +1,6 @@ module Echidna where import Control.Monad.Catch (MonadThrow(..)) -import Data.HashMap.Strict qualified as HM import Data.List (find) import Data.List.NonEmpty (NonEmpty) import Data.List.NonEmpty qualified as NE @@ -88,7 +87,7 @@ prepareContract env contracts solFiles specifiedContract seed = do loadInitialCorpus :: Env -> World -> IO [[Tx]] loadInitialCorpus env world = do -- load transactions from init sequence (if any) - let sigs = Set.fromList $ concatMap NE.toList (HM.elems world.highSignatureMap) + let sigs = Set.fromList $ concatMap NE.toList (Map.elems world.highSignatureMap) ethenoCorpus <- case env.cfg.solConf.initialize of Nothing -> pure [] diff --git a/lib/Echidna/ABI.hs b/lib/Echidna/ABI.hs index ec25a3038..f3fbb5037 100644 --- a/lib/Echidna/ABI.hs +++ b/lib/Echidna/ABI.hs @@ -1,6 +1,3 @@ -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DerivingStrategies #-} - module Echidna.ABI where import Control.Monad (liftM2, liftM3, foldM, replicateM) @@ -15,9 +12,8 @@ import Data.ByteString (ByteString) import Data.ByteString qualified as BS import Data.DoubleWord (Int256, Word256) import Data.Foldable (toList) -import Data.Hashable (Hashable(..)) -import Data.HashMap.Strict (HashMap) -import Data.HashMap.Strict qualified as M +import Data.Map (Map) +import Data.Map qualified as Map import Data.Set (Set) import Data.Set qualified as Set import Data.List (intercalate) @@ -30,7 +26,6 @@ import Data.Text.Encoding (encodeUtf8) import Data.Text.Encoding qualified as TE import Data.Vector (Vector) import Data.Vector qualified as V -import Data.Vector.Instances () import Data.Word (Word8) import Numeric (showHex) @@ -108,9 +103,9 @@ hashSig = abiKeccak . TE.encodeUtf8 data GenDict = GenDict { pSynthA :: Float -- ^ Fraction of time to use dictionary vs. synthesize - , constants :: !(HashMap AbiType (Set AbiValue)) + , constants :: !(Map AbiType (Set AbiValue)) -- ^ Constants to use, sorted by type - , wholeCalls :: !(HashMap SolSignature (Set SolCall)) + , wholeCalls :: !(Map SolSignature (Set SolCall)) -- ^ Whole calls to use, sorted by type , defSeed :: Int -- ^ Default seed to use if one is not provided in EConfig @@ -121,20 +116,16 @@ data GenDict = GenDict } hashMapBy - :: (Hashable k, Hashable a, Eq k, Ord a) + :: (Ord k, Eq k, Ord a) => (a -> k) -> Set a - -> HashMap k (Set a) -hashMapBy f = M.fromListWith Set.union . fmap (\v -> (f v, Set.singleton v)) . Set.toList + -> Map k (Set a) +hashMapBy f = Map.fromListWith Set.union . fmap (\v -> (f v, Set.singleton v)) . Set.toList gaddCalls :: Set SolCall -> GenDict -> GenDict gaddCalls calls dict = dict { wholeCalls = dict.wholeCalls <> hashMapBy (fmap $ fmap abiValueType) calls } -deriving anyclass instance Hashable AbiType -deriving anyclass instance Hashable AbiValue -deriving anyclass instance Hashable Addr - -- | Construct a 'GenDict' from some dictionaries, a 'Float', a default seed, -- and a typing rule for return values mkGenDict @@ -317,12 +308,17 @@ mutateAbiCall = traverse f -- | Given a generator taking an @a@ and returning a @b@ and a way to get @b@s associated with some -- @a@ from a 'GenDict', return a generator that takes an @a@ and either synthesizes new @b@s with the -- provided generator or uses the 'GenDict' dictionary (when available). -genWithDict :: (Eq a, Hashable a, MonadRandom m) - => GenDict -> HashMap a (Set b) -> (a -> m b) -> a -> m b +genWithDict + :: (Eq a, Ord a, MonadRandom m) + => GenDict + -> Map a (Set b) + -> (a -> m b) + -> a + -> m b genWithDict genDict m g t = do r <- getRandom let maybeValM = if genDict.pSynthA >= r then fromDict else pure Nothing - fromDict = case M.lookup t m of + fromDict = case Map.lookup t m of Nothing -> pure Nothing Just cs -> Just <$> rElem' cs fromMaybe <$> g t <*> maybeValM @@ -362,9 +358,15 @@ genAbiCallM genDict abi = do abi mutateAbiCall solCall --- | Given a list of 'SolSignature's, generate a random 'SolCall' for one, possibly with a dictionary. -genInteractionsM :: MonadRandom m => GenDict -> NonEmpty SolSignature -> m SolCall -genInteractionsM genDict l = genAbiCallM genDict =<< rElem l +-- | Given a list of 'SolSignature's, generate a random 'SolCall' for one, +-- possibly with a dictionary. +genInteractionsM + :: MonadRandom m + => GenDict + -> NonEmpty SolSignature + -> m SolCall +genInteractionsM genDict solSignatures = + rElem solSignatures >>= genAbiCallM genDict abiCalldata :: Text -> Vector AbiValue -> ByteString abiCalldata s xs = BSLazy.toStrict . runPut $ do diff --git a/lib/Echidna/Campaign.hs b/lib/Echidna/Campaign.hs index 59633cbd4..8729eeefa 100644 --- a/lib/Echidna/Campaign.hs +++ b/lib/Echidna/Campaign.hs @@ -14,7 +14,6 @@ import Control.Monad.Trans (lift) import Control.Monad.Trans.Random.Strict (liftCatch) import Data.Binary.Get (runGetOrFail) import Data.ByteString.Lazy qualified as LBS -import Data.HashMap.Strict qualified as H import Data.IORef (readIORef, writeIORef) import Data.Map qualified as Map import Data.Map (Map, (\\)) @@ -162,11 +161,12 @@ execTxOptC execTxOptC tx = do (vm, camp@Campaign{coverage = oldCov}) <- get ((res, txCov), vm') <- runStateT (execTxWithCov tx) vm - let vmr = getResult $ fst res - -- Update the tx coverage map with the proper binary according to the vm result - let txCov' = Map.mapWithKey (\_ s -> Set.map (set _4 vmr) s) txCov - -- Update the global coverage map with the one from this tx run - let newCov = Map.unionWith Set.union oldCov txCov' + let + vmr = getResult $ fst res + -- Update the tx coverage map with the proper binary according to the vm result + txCov' = Map.mapWithKey (\_ s -> Set.map (set _4 vmr) s) txCov + -- Update the global coverage map with the one from this tx run + newCov = Map.unionWith Set.union oldCov txCov' put (vm', camp { coverage = newCov }) when (coveragePoints oldCov < coveragePoints newCov) $ do let dict' = case tx.call of @@ -220,13 +220,16 @@ callseq initialCorpus vm world seqLen = do conf <- asks (.cfg.campaignConf) -- First, we figure out whether we need to execute with or without coverage -- optimization and gas info, and pick our execution function appropriately - let coverageEnabled = isJust conf.knownCoverage - let ef = if coverageEnabled - then execTxOptC - else \tx -> do (v, ca) <- get - (r, vm') <- runStateT (execTx tx) v - put (vm', ca) - pure r + let + coverageEnabled = isJust conf.knownCoverage + execFunc = + if coverageEnabled + then execTxOptC + else \tx -> do + (v, ca) <- get + (r, vm') <- runStateT (execTx tx) v + put (vm', ca) + pure r -- Then, we get the current campaign state camp <- get -- Then, we generate the actual transaction in the sequence @@ -234,25 +237,25 @@ callseq initialCorpus vm world seqLen = do metaCache <- liftIO $ readIORef metaCacheRef -- Replay transactions in the corpus during the first iterations txSeq <- if length initialCorpus > camp.ncallseqs - then pure $ initialCorpus !! camp.ncallseqs - else randseq metaCache seqLen vm._env._contracts world + then pure $ initialCorpus !! camp.ncallseqs + else randseq metaCache seqLen vm._env._contracts world -- We then run each call sequentially. This gives us the result of each call, plus a new state - (res, (vm', camp')) <- runStateT (evalSeq vm ef txSeq) (vm, camp) + (res, (vm', camp')) <- runStateT (evalSeq vm execFunc txSeq) (vm, camp) let -- compute the addresses not present in the old VM via set difference newAddrs = Map.keys $ vm'._env._contracts \\ vm._env._contracts -- and construct a set to union to the constants table - diffs = H.fromList [(AbiAddressType, Set.fromList $ AbiAddress <$> newAddrs)] + diffs = Map.fromList [(AbiAddressType, Set.fromList $ AbiAddress <$> newAddrs)] -- Now we try to parse the return values as solidity constants, and add then to the 'GenDict' - results = parse (map (\(t, (vr, _)) -> (t, vr)) res) camp.genDict.rTypes + results = returnValues (map (\(t, (vr, _)) -> (t, vr)) res) camp.genDict.rTypes -- union the return results with the new addresses - additions = H.unionWith Set.union diffs results + additions = Map.unionWith Set.union diffs results -- append to the constants dictionary updatedDict = camp.genDict - { constants = H.unionWith Set.union additions camp.genDict.constants - , dictValues = Set.union (mkDictValues $ Set.unions $ H.elems additions) + { constants = Map.unionWith Set.union additions camp.genDict.constants + , dictValues = Set.union (mkDictValues $ Set.unions $ Map.elems additions) camp.genDict.dictValues } @@ -274,16 +277,17 @@ callseq initialCorpus vm world seqLen = do , ncallseqs = camp.ncallseqs + 1 } where - -- Given a list of transactions and a return typing rule, this checks whether we know the return - -- type for each function called, and if we do, tries to parse the return value as a value of - -- that type. It returns a 'GenDict' style HashMap. - parse l rt = H.fromList . flip mapMaybe l $ \(tx, result) -> do - fname <- case tx.call of - SolCall (fname, _) -> Just fname - _ -> Nothing - type' <- rt fname + -- Given a list of transactions and a return typing rule, this checks whether we know the return + -- type for each function called, and if we do, tries to parse the return value as a value of + -- that type. It returns a 'GenDict' style Map. + returnValues txResults returnTypeOf = + Map.fromList . flip mapMaybe txResults $ \(tx, result) -> do case result of - VMSuccess (ConcreteBuf buf) -> + VMSuccess (ConcreteBuf buf) -> do + fname <- case tx.call of + SolCall (fname, _) -> Just fname + _ -> Nothing + type' <- returnTypeOf fname case runGetOrFail (getAbi type') (LBS.fromStrict buf) of -- make sure we don't use cheat codes to form fuzzing call sequences Right (_, _, abiValue) | abiValue /= AbiAddress cheatCode -> @@ -305,17 +309,19 @@ campaign -> m Campaign campaign u vm world ts dict initialCorpus = do conf <- asks (.cfg.campaignConf) - metaCacheRef <- asks (.metadataCache) fetchContractCacheRef <- asks (.fetchContractCache) external <- liftIO $ Map.mapMaybe id <$> readIORef fetchContractCacheRef liftIO $ writeIORef metaCacheRef (memo (vm._env._contracts <> external)) - let c = fromMaybe mempty conf.knownCoverage - let effectiveSeed = fromMaybe dict.defSeed conf.seed - effectiveGenDict = dict { defSeed = effectiveSeed } - camp = Campaign ts c mempty effectiveGenDict False Set.empty 0 + let + covMap = fromMaybe mempty conf.knownCoverage + effectiveSeed = fromMaybe dict.defSeed conf.seed + effectiveGenDict = dict { defSeed = effectiveSeed } + camp = Campaign ts covMap mempty effectiveGenDict False Set.empty 0 + execStateT (evalRandT (lift u >> runCampaign) (mkStdGen effectiveSeed)) camp + where memo = makeBytecodeCache . map (forceBuf . (^. bytecode)) . Map.elems runCampaign = do diff --git a/lib/Echidna/Config.hs b/lib/Echidna/Config.hs index 89c866ab7..32a9f0a8e 100644 --- a/lib/Echidna/Config.hs +++ b/lib/Echidna/Config.hs @@ -8,7 +8,6 @@ import Data.Aeson.KeyMap (keys) import Data.Bool (bool) import Data.ByteString qualified as BS import Data.Functor ((<&>)) -import Data.HashSet (fromList, insert, difference) import Data.Maybe (fromMaybe) import Data.Set qualified as Set import Data.Text (isPrefixOf) @@ -39,9 +38,9 @@ instance FromJSON EConfigWithUsage where let v' = case o of Object v -> v _ -> mempty - (c, ks) <- runStateT (parser v') $ fromList [] - let found = fromList (keys v') - pure $ EConfigWithUsage c (found `difference` ks) (ks `difference` found) + (c, ks) <- runStateT (parser v') $ Set.fromList [] + let found = Set.fromList (keys v') + pure $ EConfigWithUsage c (found `Set.difference` ks) (ks `Set.difference` found) -- this parser runs in StateT and comes equipped with the following -- equivalent unary operators: -- x .:? k (Parser) <==> x ..:? k (StateT) @@ -58,7 +57,7 @@ instance FromJSON EConfigWithUsage where <*> v ..:? "rpcUrl" <*> v ..:? "rpcBlock" where - useKey k = modify' $ insert k + useKey k = modify' $ Set.insert k x ..:? k = useKey k >> lift (x .:? k) x ..!= y = fromMaybe y <$> x -- Parse as unbounded Integer and see if it fits into W256 diff --git a/lib/Echidna/Deploy.hs b/lib/Echidna/Deploy.hs index c79b63d2f..a9df333bf 100644 --- a/lib/Echidna/Deploy.hs +++ b/lib/Echidna/Deploy.hs @@ -1,6 +1,8 @@ module Echidna.Deploy where +import Control.Monad (foldM) import Control.Monad.Catch (MonadThrow(..), throwM) +import Control.Monad.Reader (MonadReader, asks) import Control.Monad.State.Strict (execStateT, MonadIO) import Data.ByteString (ByteString) import Data.ByteString qualified as BS @@ -9,16 +11,15 @@ import Data.Either (fromRight) import Data.Text (Text, unlines) import Data.Text.Encoding (encodeUtf8) -import EVM hiding (Env) +import EVM hiding (bytecode, Env) import EVM.Solidity import EVM.Types (Addr) -import Echidna.Types.Solidity (SolException(..)) -import Echidna.Types.Tx (createTx, unlimitedGasPerBlock) import Echidna.Exec (execTx) import Echidna.Events (extractEvents) -import Control.Monad.Reader (MonadReader, asks) import Echidna.Types.Config (Env(..)) +import Echidna.Types.Solidity (SolException(..)) +import Echidna.Types.Tx (createTx, unlimitedGasPerBlock) deployContracts :: (MonadIO m, MonadReader Env m, MonadThrow m) @@ -46,17 +47,15 @@ deployBytecodes' -> Addr -> VM -> m VM -deployBytecodes' [] _ vm = pure vm -deployBytecodes' ((a, bc):cs) d vm = - deployBytecodes' cs d =<< loadRest +deployBytecodes' cs src initialVM = foldM deployOne initialVM cs where - -- This will initialize with zero a large number of possible constructor parameters - zeros = BS.replicate 320 0 - loadRest = do - vm' <- flip execStateT vm $ - execTx $ createTx (bc <> zeros) d a unlimitedGasPerBlock (0, 0) - case vm'._result of - Just (VMSuccess _) -> pure vm' - _ -> do - di <- asks (.dapp) - throwM $ DeploymentFailed a (Data.Text.unlines $ extractEvents True di vm') + deployOne vm (dst, bytecode) = do + vm' <- flip execStateT vm $ + execTx $ createTx (bytecode <> zeros) src dst unlimitedGasPerBlock (0, 0) + case vm'._result of + Just (VMSuccess _) -> pure vm' + _ -> do + di <- asks (.dapp) + throwM $ DeploymentFailed dst (Data.Text.unlines $ extractEvents True di vm') + -- This will initialize with zero a large number of possible constructor parameters + zeros = BS.replicate 320 0 diff --git a/lib/Echidna/Exec.hs b/lib/Echidna/Exec.hs index 1bb8b84b2..1ea10b5e7 100644 --- a/lib/Echidna/Exec.hs +++ b/lib/Echidna/Exec.hs @@ -41,14 +41,15 @@ data ErrorClass = RevertE | IllegalE | UnknownE -- | Given an execution error, classify it. Mostly useful for nice @pattern@s ('Reversion', 'Illegal'). classifyError :: Error -> ErrorClass -classifyError (OutOfGas _ _) = RevertE -classifyError (Revert _) = RevertE -classifyError (UnrecognizedOpcode _) = RevertE -classifyError StackLimitExceeded = RevertE -classifyError StackUnderrun = IllegalE -classifyError BadJumpDestination = IllegalE -classifyError IllegalOverflow = IllegalE -classifyError _ = UnknownE +classifyError = \case + OutOfGas _ _ -> RevertE + Revert _ -> RevertE + UnrecognizedOpcode _ -> RevertE + StackLimitExceeded -> RevertE + StackUnderrun -> IllegalE + BadJumpDestination -> IllegalE + IllegalOverflow -> IllegalE + _ -> UnknownE -- | Extracts the 'Query' if there is one. getQuery :: VMResult -> Maybe Query @@ -65,7 +66,8 @@ pattern Illegal <- VMFailure (classifyError -> IllegalE) -- | Given an execution error, throw the appropriate exception. vmExcept :: MonadThrow m => Error -> m () -vmExcept e = throwM $ case VMFailure e of {Illegal -> IllegalExec e; _ -> UnknownFailure e} +vmExcept e = throwM $ + case VMFailure e of {Illegal -> IllegalExec e; _ -> UnknownFailure e} -- | Given an error handler `onErr`, an execution strategy `executeTx`, and a transaction `tx`, -- execute that transaction using the given execution strategy, calling `onErr` on errors. diff --git a/lib/Echidna/Mutator/Array.hs b/lib/Echidna/Mutator/Array.hs index 95f0a8adc..a756c1dda 100644 --- a/lib/Echidna/Mutator/Array.hs +++ b/lib/Echidna/Mutator/Array.hs @@ -5,14 +5,24 @@ import Data.ListLike qualified as LL -- | A list of mutators to randomly select to perform a mutation of list-like values listMutators :: (LL.ListLike f i, MonadRandom m) => m (f -> m f) -listMutators = fromList [(return, 1), (expandRandList, 10), (deleteRandList, 10), (swapRandList, 10)] +listMutators = fromList + [ (pure, 1) -- no-op + , (expandRandList, 10) + , (deleteRandList, 10) + , (swapRandList, 10) + ] -- | Mutate a list-like data structure using a list of mutators -mutateLL :: (LL.ListLike f i, MonadRandom m) - => Maybe Int -- ^ Required size for the mutated list-like value (or Nothing if there are no constrains) - -> f -- ^ Randomly generated list-like value to complement the mutated list, if it is shorter than the requested size - -> f -- ^ List-like value to mutate - -> m f +mutateLL + :: (LL.ListLike f i, MonadRandom m) + -- | Required size for the mutated list-like value (or Nothing if there are no constrains) + => Maybe Int + -- | Randomly generated list-like value to complement the mutated list, if it is + -- shorter than the requested size + -> f + -- | List-like value to mutate + -> f + -> m f mutateLL mn fs vs = do f <- listMutators xs <- f vs @@ -24,10 +34,11 @@ replaceAt i f n = LL.take n f <> LL.cons i (LL.drop (n + 1) f) expandAt :: LL.ListLike f i => f -> Int -> Int -> f expandAt xs k t = case LL.uncons xs of - Nothing -> xs - Just (y,ys) -> if k == 0 - then LL.replicate t y <> ys - else LL.cons y (expandAt ys (k - 1) t) + Nothing -> xs + Just (y,ys) -> + if k == 0 + then LL.replicate t y <> ys + else LL.cons y (expandAt ys (k - 1) t) expandRandList :: (LL.ListLike f i, MonadRandom m) => f -> m f expandRandList xs diff --git a/lib/Echidna/Mutator/Corpus.hs b/lib/Echidna/Mutator/Corpus.hs index e6c61656f..757044c4e 100644 --- a/lib/Echidna/Mutator/Corpus.hs +++ b/lib/Echidna/Mutator/Corpus.hs @@ -1,7 +1,8 @@ module Echidna.Mutator.Corpus where import Control.Monad.Random.Strict (MonadRandom, getRandomR, weighted) -import Data.Set qualified as DS +import Data.Set (Set) +import Data.Set qualified as Set import Echidna.Mutator.Array import Echidna.Transaction (mutateTx, shrinkTx) @@ -37,37 +38,58 @@ mutator Expansion = expandRandList mutator Swapping = swapRandList mutator Deletion = deleteRandList -selectAndMutate :: MonadRandom m - => ([Tx] -> m [Tx]) -> Corpus -> m [Tx] -selectAndMutate f ctxs = do - rtxs <- weighted $ map (\(i, txs) -> (txs, fromIntegral i)) $ DS.toDescList ctxs +selectAndMutate + :: MonadRandom m + => ([Tx] -> m [Tx]) + -> Corpus + -> m [Tx] +selectAndMutate f corpus = do + rtxs <- selectFromCorpus corpus k <- getRandomR (0, length rtxs - 1) f $ take k rtxs -selectAndCombine :: MonadRandom m - => ([Tx] -> [Tx] -> m [Tx]) -> Int -> Corpus -> [Tx] -> m [Tx] -selectAndCombine f ql ctxs gtxs = do - rtxs1 <- selectFromCorpus - rtxs2 <- selectFromCorpus +selectAndCombine + :: MonadRandom m + => ([Tx] -> [Tx] -> m [Tx]) + -> Int + -> Corpus + -> [Tx] + -> m [Tx] +selectAndCombine f ql corpus gtxs = do + rtxs1 <- selectFromCorpus corpus + rtxs2 <- selectFromCorpus corpus txs <- f rtxs1 rtxs2 - return . take ql $ txs ++ gtxs - where selectFromCorpus = weighted $ map (\(i, txs) -> (txs, fromIntegral i)) $ DS.toDescList ctxs - -getCorpusMutation :: MonadRandom m - => CorpusMutation -> (Int -> Corpus -> [Tx] -> m [Tx]) + pure . take ql $ txs <> gtxs + +selectFromCorpus + :: MonadRandom m + => Set (Int, [Tx]) + -> m [Tx] +selectFromCorpus = + weighted . map (\(i, txs) -> (txs, fromIntegral i)) . Set.toDescList + +getCorpusMutation + :: MonadRandom m + => CorpusMutation + -> (Int -> Corpus -> [Tx] -> m [Tx]) getCorpusMutation (RandomAppend m) = mut (mutator m) - where mut f ql ctxs gtxs = do - rtxs' <- selectAndMutate f ctxs - return . take ql $ rtxs' ++ gtxs + where + mut f ql ctxs gtxs = do + rtxs' <- selectAndMutate f ctxs + pure . take ql $ rtxs' ++ gtxs getCorpusMutation (RandomPrepend m) = mut (mutator m) - where mut f ql ctxs gtxs = do - rtxs' <- selectAndMutate f ctxs - k <- getRandomR (0, ql - 1) - return . take ql $ take k gtxs ++ rtxs' + where + mut f ql ctxs gtxs = do + rtxs' <- selectAndMutate f ctxs + k <- getRandomR (0, ql - 1) + pure . take ql $ take k gtxs ++ rtxs' getCorpusMutation RandomSplice = selectAndCombine spliceAtRandom getCorpusMutation RandomInterleave = selectAndCombine interleaveAtRandom -seqMutatorsStateful :: MonadRandom m => MutationConsts Rational -> m CorpusMutation +seqMutatorsStateful + :: MonadRandom m + => MutationConsts Rational + -> m CorpusMutation seqMutatorsStateful (c1, c2, c3, c4) = weighted [(RandomAppend Identity, 800), (RandomPrepend Identity, 200), @@ -88,7 +110,10 @@ seqMutatorsStateful (c1, c2, c3, c4) = weighted (RandomInterleave, c4) ] -seqMutatorsStateless :: MonadRandom m => MutationConsts Rational -> m CorpusMutation +seqMutatorsStateless + :: MonadRandom m + => MutationConsts Rational + -> m CorpusMutation seqMutatorsStateless (c1, c2, _, _) = weighted [(RandomAppend Identity, 800), (RandomPrepend Identity, 200), diff --git a/lib/Echidna/Orphans/JSON.hs b/lib/Echidna/Orphans/JSON.hs index 9ff64672a..ddfa15b06 100644 --- a/lib/Echidna/Orphans/JSON.hs +++ b/lib/Echidna/Orphans/JSON.hs @@ -11,7 +11,7 @@ import Control.Monad.Fail (fail) import Data.Aeson (ToJSON(..), FromJSON(..), withText) import Data.Aeson.TH (deriveJSON, defaultOptions) import Data.ByteString (ByteString) -import Data.DoubleWord (Word256, Int256, Word160) +import Data.DoubleWord (Word256, Int256) import Data.Text (Text, unpack) import EVM.ABI (AbiValue, AbiType) import Text.Read (readMaybe) @@ -31,12 +31,6 @@ instance ToJSON Int256 where instance FromJSON Int256 where parseJSON = withText "Int256" $ maybe (fail "could not parse Int256") pure . readT -instance ToJSON Word160 where - toJSON = toJSON . show - -instance FromJSON Word160 where - parseJSON = withText "Int160" $ maybe (fail "could not parse Word160") pure . readT - instance ToJSON ByteString where toJSON = toJSON . show diff --git a/lib/Echidna/Output/Corpus.hs b/lib/Echidna/Output/Corpus.hs index 032d8c8a6..c1df732fc 100644 --- a/lib/Echidna/Output/Corpus.hs +++ b/lib/Echidna/Output/Corpus.hs @@ -1,8 +1,6 @@ module Echidna.Output.Corpus where -import Prelude hiding (Word) - -import Control.Monad (unless) +import Control.Monad.Extra (unlessM) import Data.Aeson (ToJSON(..), decodeStrict, encodeFile) import Data.ByteString qualified as BS import Data.Hashable (hash) @@ -10,21 +8,21 @@ import Data.Maybe (catMaybes) import System.Directory (createDirectoryIfMissing, makeRelativeToCurrentDirectory, doesFileExist) import System.FilePath ((), (<.>)) -import Echidna.Types.Tx -import Echidna.Output.Utils +import Echidna.Types.Tx (Tx) +import Echidna.Utility (listDirectory, withCurrentDirectory) saveTxs :: FilePath -> [[Tx]] -> IO () -saveTxs d = mapM_ saveTx where - saveTx v = do let fn = d (show . hash . show) v <.> "txt" - b <- doesFileExist fn - unless b $ encodeFile fn (toJSON v) +saveTxs dir = mapM_ saveTxSeq where + saveTxSeq txSeq = do + let file = dir (show . hash . show) txSeq <.> "txt" + unlessM (doesFileExist file) $ encodeFile file (toJSON txSeq) loadTxs :: FilePath -> IO [[Tx]] loadTxs dir = do createDirectoryIfMissing True dir - fs <- listDirectory dir - css <- mapM readCall <$> mapM makeRelativeToCurrentDirectory fs - txs <- catMaybes <$> withCurrentDirectory dir css - putStrLn ("Loaded total of " ++ show (length txs) ++ " transactions from " ++ dir) - return txs + files <- listDirectory dir + css <- mapM readCall <$> mapM makeRelativeToCurrentDirectory files + txSeqs <- catMaybes <$> withCurrentDirectory dir css + putStrLn ("Loaded " ++ show (length txSeqs) ++ " transaction sequences from " ++ dir) + pure txSeqs where readCall f = decodeStrict <$> BS.readFile f diff --git a/lib/Echidna/Output/JSON.hs b/lib/Echidna/Output/JSON.hs index e916a4138..bb8ba9b96 100644 --- a/lib/Echidna/Output/JSON.hs +++ b/lib/Echidna/Output/JSON.hs @@ -1,4 +1,3 @@ -{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE RecordWildCards #-} module Echidna.Output.JSON where @@ -6,8 +5,9 @@ module Echidna.Output.JSON where import Data.Aeson hiding (Error) import Data.ByteString.Base16 qualified as BS16 import Data.ByteString.Lazy (ByteString) -import Data.Foldable qualified as DF -import Data.Map +import Data.Map (Map) +import Data.Map qualified as Map +import Data.Set qualified as Set import Data.Text import Data.Text.Encoding (decodeUtf8) import Numeric (showHex) @@ -95,24 +95,26 @@ instance ToJSON Transaction where encodeCampaign :: C.Campaign -> ByteString encodeCampaign C.Campaign{..} = encode - Campaign { _success = True - , _error = Nothing - , _tests = mapTest <$> tests - , seed = genDict.defSeed - , coverage = mapKeys (("0x" ++) . (`showHex` "") . keccak') $ DF.toList <$> coverage - , gasInfo = toList gasInfo - } + Campaign + { _success = True + , _error = Nothing + , _tests = mapTest <$> tests + , seed = genDict.defSeed + , coverage = Map.mapKeys (("0x" ++) . (`showHex` "") . keccak') $ Set.toList <$> coverage + , gasInfo = Map.toList gasInfo + } mapTest :: EchidnaTest -> Test mapTest test = - let (status, transactions, err) = mapTestState test.state test.reproducer in - Test { contract = "" -- TODO add when mapping is available https://github.com/crytic/echidna/issues/415 - , name = "name" --TODO add a proper name here - , status = status - , _error = err - , testType = Property - , transactions = transactions - } + let (status, transactions, err) = mapTestState test.state test.reproducer + in Test + { contract = "" -- TODO add when mapping is available https://github.com/crytic/echidna/issues/415 + , name = "name" -- TODO add a proper name here + , status = status + , _error = err + , testType = Property + , transactions = transactions + } where mapTestState (T.Open _) _ = (Fuzzing, Nothing, Nothing) mapTestState T.Passed _ = (Passed, Nothing, Nothing) @@ -121,15 +123,17 @@ mapTest test = mapTestState (T.Failed e) _ = (Error, Nothing, Just $ show e) -- TODO add (show e) mapTx tx = - let (function, args) = mapCall tx.call in - Transaction { contract = "" -- TODO add when mapping is available https://github.com/crytic/echidna/issues/415 - , function = function - , arguments = args - , gas = toInteger tx.gas - , gasprice = toInteger tx.gasprice - } - - mapCall (SolCreate _) = ("", Nothing) - mapCall (SolCall (name, args)) = (name, Just $ ppAbiValue <$> args) - mapCall NoCall = ("*wait*", Nothing) - mapCall (SolCalldata x) = (decodeUtf8 $ "0x" <> BS16.encode x, Nothing) + let (function, args) = mapCall tx.call + in Transaction + { contract = "" -- TODO add when mapping is available https://github.com/crytic/echidna/issues/415 + , function = function + , arguments = args + , gas = toInteger tx.gas + , gasprice = toInteger tx.gasprice + } + + mapCall = \case + SolCreate _ -> ("", Nothing) + SolCall (name, args) -> (name, Just $ ppAbiValue <$> args) + NoCall -> ("*wait*", Nothing) + SolCalldata x -> (decodeUtf8 $ "0x" <> BS16.encode x, Nothing) diff --git a/lib/Echidna/Output/Utils.hs b/lib/Echidna/Output/Utils.hs deleted file mode 100644 index 780f0cde8..000000000 --- a/lib/Echidna/Output/Utils.hs +++ /dev/null @@ -1,16 +0,0 @@ -module Echidna.Output.Utils where - -import Control.Monad.Catch (bracket) -import System.Directory (getDirectoryContents, getCurrentDirectory, setCurrentDirectory) - -listDirectory :: FilePath -> IO [FilePath] -listDirectory path = filter f <$> getDirectoryContents path - where f filename = filename /= "." && filename /= ".." - -withCurrentDirectory :: FilePath -- ^ Directory to execute in - -> IO a -- ^ Action to be executed - -> IO a -withCurrentDirectory dir action = - bracket getCurrentDirectory setCurrentDirectory $ \_ -> do - setCurrentDirectory dir - action diff --git a/lib/Echidna/Processor.hs b/lib/Echidna/Processor.hs index 7dc418a3c..6d1be24d6 100644 --- a/lib/Echidna/Processor.hs +++ b/lib/Echidna/Processor.hs @@ -10,9 +10,10 @@ import Data.ByteString.Base16 qualified as BS16 (decode) import Data.ByteString.Lazy.Char8 qualified as BSL import Data.ByteString.UTF8 qualified as BSU import Data.Either (fromRight) -import Data.HashMap.Strict qualified as M import Data.List (isPrefixOf) import Data.List.NonEmpty qualified as NE +import Data.Map (Map) +import Data.Map qualified as Map import Data.Maybe (catMaybes, fromMaybe) import Data.SemVer (Version, fromText) import Data.Set (Set) @@ -45,16 +46,16 @@ 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 ContractName -> M.HashMap ContractName [FunctionName] -> [FunctionHash] +filterResults :: Maybe ContractName -> Map ContractName [FunctionName] -> [FunctionHash] filterResults (Just c) rs = - case M.lookup c rs of + case Map.lookup c rs of Nothing -> filterResults Nothing rs Just s -> hashSig <$> s -filterResults Nothing rs = hashSig <$> (concat . M.elems) rs +filterResults Nothing rs = hashSig <$> (concat . Map.elems) rs enhanceConstants :: SlitherInfo -> Set AbiValue enhanceConstants si = - Set.fromList . concatMap enh . concat . concat . M.elems $ M.elems <$> si.constantValues + Set.fromList . concatMap enh . concat . concat . Map.elems $ Map.elems <$> si.constantValues where enh (AbiUInt _ n) = makeNumAbiValues (fromIntegral n) enh (AbiInt _ n) = makeNumAbiValues (fromIntegral n) @@ -63,11 +64,11 @@ enhanceConstants si = -- 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]) + { payableFunctions :: Map ContractName [FunctionName] + , constantFunctions :: Map ContractName [FunctionName] + , asserts :: Map ContractName [FunctionName] + , constantValues :: Map ContractName (Map FunctionName [AbiValue]) + , generationGraph :: Map ContractName (Map FunctionName [FunctionName]) , solcVersions :: [Version] , fallbackDefined :: [ContractName] , receiveDefined :: [ContractName] @@ -90,7 +91,7 @@ instance FromJSON SlitherInfo where receiveDefined <- o .:? "with_receive" .!= ["*"] constantValues' -- the type annotation is needed - :: M.HashMap ContractName (M.HashMap FunctionName [[Maybe AbiValue]]) + :: Map ContractName (Map 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' diff --git a/lib/Echidna/Shrink.hs b/lib/Echidna/Shrink.hs index 922a7cc8f..7392b66a2 100644 --- a/lib/Echidna/Shrink.hs +++ b/lib/Echidna/Shrink.hs @@ -58,24 +58,22 @@ shrinkSeq -> TestValue -> [Tx] -> m (Maybe ([Tx], TestValue, VM)) -shrinkSeq f v xs = do - strategies <- sequence [shorten, shrunk] - let strategy = uniform strategies - xs' <- strategy - (value, vm') <- check xs' +shrinkSeq f v txs = do + txs' <- uniform =<< sequence [shorten, shrunk] + (value, vm') <- check txs' -- if the test passed it means we didn't shrink successfully pure $ case (value,v) of - (BoolValue False, _) -> Just (xs', value, vm') - (IntValue x, IntValue y) | x >= y -> Just (xs', value, vm') + (BoolValue False, _) -> Just (txs', value, vm') + (IntValue x, IntValue y) | x >= y -> Just (txs', value, vm') _ -> Nothing where check xs' = do - og <- get + vm <- get res <- traverse_ execTx xs' >> f - put og + put vm pure res - shrunk = mapM (shrinkSender <=< shrinkTx) xs - shorten = (\i -> take i xs ++ drop (i + 1) xs) <$> getRandomR (0, length xs) + shrunk = mapM (shrinkSender <=< shrinkTx) txs + shorten = (\i -> take i txs ++ drop (i + 1) txs) <$> getRandomR (0, length txs) shrinkSender :: (MonadReader Env m, MonadRandom m) => Tx -> m Tx shrinkSender x = do diff --git a/lib/Echidna/Solidity.hs b/lib/Echidna/Solidity.hs index 72979bc76..cac244f90 100644 --- a/lib/Echidna/Solidity.hs +++ b/lib/Echidna/Solidity.hs @@ -8,7 +8,6 @@ import Control.Monad.Extra (whenM) import Control.Monad.Reader (ReaderT(runReaderT)) import Control.Monad.State.Strict (execStateT) import Data.Foldable (toList) -import Data.HashMap.Strict qualified as M import Data.List (find, partition, isSuffixOf, (\\)) import Data.List.NonEmpty (NonEmpty((:|))) import Data.List.NonEmpty qualified as NE @@ -200,8 +199,8 @@ loadSpecified env name cs = do unless solConf.quiet $ putStrLn $ "Analyzing contract: " <> T.unpack mainContract.contractName - -- generate the complete abi mapping let + -- generate the complete abi mapping abi = Map.elems mainContract.abiMap <&> \method -> (method.name, snd <$> method.inputs) (tests, funs) = partition (isPrefixOf solConf.prefix . fst) abi @@ -215,14 +214,14 @@ loadSpecified env name cs = do -- Construct ABI mapping for World abiMapping = if solConf.allContracts then - M.fromList $ catMaybes $ cs <&> \contract -> + Map.fromList $ catMaybes $ cs <&> \contract -> let filtered = filterMethods contract.contractName solConf.methodFilter (abiOf solConf.prefix contract) in (getBytecodeMetadata contract.runtimeCode,) <$> NE.nonEmpty filtered else case NE.nonEmpty fabiOfc of - Just ne -> M.singleton (getBytecodeMetadata mainContract.runtimeCode) ne + Just ne -> Map.singleton (getBytecodeMetadata mainContract.runtimeCode) ne Nothing -> mempty -- Set up initial VM, either with chosen contract or Etheno initialization file @@ -299,9 +298,11 @@ loadSpecified env name cs = do choose _ (Just n) = maybe (throwM $ ContractNotFound n) pure $ find (Data.Text.isSuffixOf (contractId n) . (.contractName)) cs - contractId n | T.any (== ':') n = let (splitPath, splitName) = T.breakOn ":" n in - rewritePathSeparators splitPath `T.append` splitName - | otherwise = ":" `append` n + contractId n + | T.any (== ':') n = + let (splitPath, splitName) = T.breakOn ":" n + in rewritePathSeparators splitPath `T.append` splitName + | otherwise = ":" `append` n rewritePathSeparators = T.pack . joinPath . splitDirectories . T.unpack setUpFunction = ("setUp", []) @@ -328,7 +329,7 @@ filterFallbacks -> [ContractName] -> SignatureMap -> SignatureMap -filterFallbacks _ [] [] sm = M.map f sm +filterFallbacks _ [] [] sm = Map.map f sm where f ss = NE.fromList $ case NE.filter (/= fallback) ss of [] -> [fallback] -- No other alternative ss' -> ss' @@ -343,16 +344,16 @@ prepareHashMaps [] _ m = (m, Nothing) -- No constant functions detected prepareHashMaps cs as m = let (hm, lm) = - ( M.unionWith NEE.union (filterHashMap not cs m) (filterHashMap id as m) + ( Map.unionWith NEE.union (filterHashMap not cs m) (filterHashMap id as m) , filterHashMap id cs m ) in - if | M.size hm > 0 && M.size lm > 0 -> (hm, Just lm) -- Usual case - | M.size hm > 0 && M.size lm == 0 -> (hm, Nothing) -- No low-priority functions detected - | M.size hm == 0 && M.size lm > 0 -> (m, Nothing) -- No high-priority functions detected + if | Map.size hm > 0 && Map.size lm > 0 -> (hm, Just lm) -- Usual case + | Map.size hm > 0 && Map.size lm == 0 -> (hm, Nothing) -- No low-priority functions detected + | Map.size hm == 0 && Map.size lm > 0 -> (m, Nothing) -- No high-priority functions detected | otherwise -> error "Error processing function hashmaps" where filterHashMap f xs = - M.mapMaybe (NE.nonEmpty . NE.filter (\s -> f $ (hashSig . encodeSig $ s) `elem` xs)) + Map.mapMaybe (NE.nonEmpty . NE.filter (\s -> f $ (hashSig . encodeSig $ s) `elem` xs)) -- | Given a file and an optional contract name, compile the file as solidity, then, if a name is -- given, try to fine the specified contract (assuming it is in the file provided), otherwise, find @@ -370,7 +371,7 @@ loadSolTests env fp name = do (vm, funs, testNames, _signatureMap) <- loadSpecified env name contracts let eventMap = Map.unions $ map (.eventMap) contracts - world = World solConf.sender M.empty Nothing [] eventMap + world = World solConf.sender mempty Nothing [] eventMap echidnaTests = createTests solConf.testMode True testNames vm._state._contract funs pure (vm, world, echidnaTests) diff --git a/lib/Echidna/Test.hs b/lib/Echidna/Test.hs index 39f9db627..9891a6983 100644 --- a/lib/Echidna/Test.hs +++ b/lib/Echidna/Test.hs @@ -90,14 +90,21 @@ createTests -> [SolSignature] -> [EchidnaTest] createTests m td ts r ss = case m of - "exploration" -> [createTest Exploration] - "overflow" -> [createTest (CallTest "Integer (over/under)flow" checkOverflowTest)] - "property" -> map (\t -> createTest (PropertyTest t r)) ts - "optimization" -> map (\t -> createTest (OptimizationTest t r)) ts - "assertion" -> map (\s -> createTest (AssertionTest False s r)) (filter (/= fallback) ss) ++ [createTest (CallTest "AssertionFailed(..)" checkAssertionTest)] - "dapptest" -> map (\s -> createTest (AssertionTest True s r)) (filter (\(n, xs) -> T.isPrefixOf "invariant_" n || not (null xs)) ss) - _ -> error validateTestModeError - + "exploration" -> + [createTest Exploration] + "overflow" -> + [createTest (CallTest "Integer (over/under)flow" checkOverflowTest)] + "property" -> + map (\t -> createTest (PropertyTest t r)) ts + "optimization" -> + map (\t -> createTest (OptimizationTest t r)) ts + "assertion" -> + map (\s -> createTest (AssertionTest False s r)) + (filter (/= fallback) ss) ++ [createTest (CallTest "AssertionFailed(..)" checkAssertionTest)] + "dapptest" -> + map (\s -> createTest (AssertionTest True s r)) + (filter (\(n, xs) -> T.isPrefixOf "invariant_" n || not (null xs)) ss) + _ -> error validateTestModeError ++ (if td then [sdt, sdat] else []) where sdt = createTest (CallTest "Target contract is not self-destructed" $ checkSelfDestructedTarget r) @@ -198,8 +205,8 @@ checkStatefulAssertion checkStatefulAssertion sig addr = do dappInfo <- asks (.dapp) vm <- get - -- Whether the last transaction called the function `sig`. let + -- Whether the last transaction called the function `sig`. isCorrectFn = BS.isPrefixOf (BS.take 4 (abiCalldata (encodeSig sig) mempty)) (forceBuf vm._state._calldata) diff --git a/lib/Echidna/Transaction.hs b/lib/Echidna/Transaction.hs index c20d7cb82..ad26a6a38 100644 --- a/lib/Echidna/Transaction.hs +++ b/lib/Echidna/Transaction.hs @@ -6,14 +6,13 @@ module Echidna.Transaction where import Control.Lens import Control.Monad (join) import Control.Monad.Random.Strict (MonadRandom, getRandomR, uniform) -import Control.Monad.State.Strict (MonadState, gets) -import Data.HashMap.Strict qualified as M +import Control.Monad.State.Strict (MonadState, gets, modify') import Data.Map (Map, toList) import Data.Maybe (mapMaybe) import Data.Set (Set) import Data.Set qualified as Set import Data.Vector qualified as V -import EVM hiding (value) +import EVM hiding (resetState, tx, value) import EVM.ABI (abiValueType) import EVM.Types (Expr(ConcreteBuf, Lit), Addr, W256) @@ -26,6 +25,7 @@ import Echidna.Types.Signature (SignatureMap, SolCall, ContractA, FunctionHash, import Echidna.Types.Tx import Echidna.Types.World (World(..)) import Echidna.Types.Campaign (Campaign(..)) +import qualified Data.Map as Map hasSelfdestructed :: VM -> Addr -> Bool hasSelfdestructed vm addr = addr `elem` vm._tx._substate._selfdestructs @@ -77,7 +77,7 @@ genTx memo world txConf deployedContracts = do toContractA sigMap (addr, c) = let bc = forceBuf $ c ^. bytecode metadata = lookupBytecodeMetadata memo bc - in (addr,) <$> M.lookup metadata sigMap + in (addr,) <$> Map.lookup metadata sigMap genDelay :: MonadRandom m => W256 -> Set W256 -> m W256 genDelay mv ds = do @@ -139,50 +139,55 @@ shrinkTx tx' = in join $ usuallyRarely (join (uniform possibilities)) (pure $ removeCallTx tx') mutateTx :: (MonadRandom m) => Tx -> m Tx -mutateTx t@(Tx { call = SolCall c }) = do +mutateTx tx@Tx{call = SolCall c} = do f <- oftenUsually skip mutate f c - where mutate z = mutateAbiCall z >>= \c' -> pure $ t { call = SolCall c' } - skip _ = pure t -mutateTx t = pure t + where mutate z = mutateAbiCall z >>= \c' -> pure tx { call = SolCall c' } + skip _ = pure tx +mutateTx tx = pure tx -- | Given a 'Transaction', set up some 'VM' so it can be executed. Effectively, this just brings -- 'Transaction's \"on-chain\". setupTx :: MonadState VM m => Tx -> m () -setupTx (Tx NoCall _ r _ _ _ (t, b)) = fromEVM $ do - state . pc .= 0 - state . stack .= mempty - state . memory .= mempty - block . timestamp %= (\x -> Lit (forceLit x + t)) - block . number += b - loadContract r - -setupTx (Tx c s r g gp v (t, b)) = fromEVM $ do - result .= Nothing - state . pc .= 0 - state . stack .= mempty - state . memory .= mempty - state . gas .= g - tx . gasprice .= gp - tx . origin .= s - state . caller .= Lit (fromIntegral s) - state . callvalue .= Lit v - block . timestamp %= (\x -> Lit (forceLit x + t)) - block . number += b - case c of +setupTx tx@Tx{call = NoCall} = fromEVM $ do + modify' $ \vm -> vm + { _state = resetState vm._state + , _block = advanceBlock vm._block tx.delay + } + loadContract tx.dst + +setupTx tx@Tx{call} = fromEVM $ do + modify' $ \vm -> vm + { _result = Nothing + , _state = (resetState vm._state) + { _gas = tx.gas + , _caller = Lit (fromIntegral tx.src) + , _callvalue = Lit tx.value + } + , _block = advanceBlock vm._block tx.delay + , _tx = vm._tx { _gasprice = tx.gasprice, _origin = tx.src } + } + case call of SolCreate bc -> do - env . contracts . at r .= Just (initialContract (InitCode bc mempty) & set balance v) - loadContract r + env . contracts . at tx.dst .= Just (initialContract (InitCode bc mempty) & set balance tx.value) + loadContract tx.dst state . code .= RuntimeCode (ConcreteRuntimeCode bc) SolCall cd -> do incrementBalance - loadContract r + loadContract tx.dst state . calldata .= ConcreteBuf (encode cd) SolCalldata cd -> do incrementBalance - loadContract r + loadContract tx.dst state . calldata .= ConcreteBuf cd where - incrementBalance = (env . contracts . ix r . balance) += v - encode (n, vs) = abiCalldata - (encodeSig (n, abiValueType <$> vs)) $ V.fromList vs + incrementBalance = env . contracts . ix tx.dst . balance += tx.value + encode (n, vs) = abiCalldata (encodeSig (n, abiValueType <$> vs)) $ V.fromList vs + +resetState :: FrameState -> FrameState +resetState s = s { _pc = 0, _stack = mempty, _memory = mempty } + +advanceBlock :: Block -> (W256, W256) -> Block +advanceBlock blk (t,b) = + blk { _timestamp = Lit (forceLit blk._timestamp + t) + , _number = blk._number + b } diff --git a/lib/Echidna/Types/Buffer.hs b/lib/Echidna/Types/Buffer.hs index a8d9087af..9ff648128 100644 --- a/lib/Echidna/Types/Buffer.hs +++ b/lib/Echidna/Types/Buffer.hs @@ -1,9 +1,10 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE GADTs #-} + module Echidna.Types.Buffer where import Data.ByteString (ByteString) -import EVM.Types (Expr(ConcreteBuf, Lit), EType(Buf), EType(EWord), W256) +import EVM.Types (Expr(ConcreteBuf, Lit), EType(Buf, EWord), W256) forceBuf :: Expr 'Buf -> ByteString forceBuf (ConcreteBuf b) = b diff --git a/lib/Echidna/Types/Config.hs b/lib/Echidna/Types/Config.hs index cc5e645b9..4fc4ce142 100644 --- a/lib/Echidna/Types/Config.hs +++ b/lib/Echidna/Types/Config.hs @@ -1,9 +1,9 @@ module Echidna.Types.Config where import Data.Aeson.Key (Key) -import Data.HashSet (HashSet) import Data.IORef (IORef) import Data.Map (Map) +import Data.Set (Set) import Data.Text (Text) import Data.Word (Word64) @@ -43,16 +43,17 @@ data EConfig = EConfig } instance Read OutputFormat where - readsPrec _ = \case 't':'e':'x':'t':r -> [(Text, r)] - 'j':'s':'o':'n':r -> [(JSON, r)] - 'n':'o':'n':'e':r -> [(None, r)] - _ -> [] + readsPrec _ = + \case 't':'e':'x':'t':r -> [(Text, r)] + 'j':'s':'o':'n':r -> [(JSON, r)] + 'n':'o':'n':'e':r -> [(None, r)] + _ -> [] data EConfigWithUsage = EConfigWithUsage { econfig :: EConfig - , badkeys :: HashSet Key - , unsetkeys :: HashSet Key + , badkeys :: Set Key + , unsetkeys :: Set Key } data Env = Env diff --git a/lib/Echidna/Types/Signature.hs b/lib/Echidna/Types/Signature.hs index 408ce8e4f..862444e9c 100644 --- a/lib/Echidna/Types/Signature.hs +++ b/lib/Echidna/Types/Signature.hs @@ -5,7 +5,6 @@ module Echidna.Types.Signature where import Data.ByteString (ByteString) import Data.ByteString qualified as BS import Data.Foldable (find) -import Data.HashMap.Strict (HashMap) import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict qualified as M import Data.Maybe (fromMaybe) @@ -14,6 +13,7 @@ import GHC.Word (Word32) import EVM.ABI (AbiType, AbiValue) import EVM.Types (Addr) +import Data.Map (Map) -- | Name of the contract type ContractName = Text @@ -29,15 +29,15 @@ type SolSignature = (FunctionName, [AbiType]) -- | Represents a call to a Solidity function. -- A tuple for the name of the function and then any 'AbiValue' arguments passed (as a list). -type SolCall = (FunctionName, [AbiValue]) +type SolCall = (FunctionName, [AbiValue]) -- | A contract is just an address with an ABI (for our purposes). type ContractA = (Addr, NonEmpty SolSignature) -- | Used to memoize results of getBytecodeMetadata -type MetadataCache = M.Map ByteString ByteString +type MetadataCache = Map ByteString ByteString -type SignatureMap = HashMap ByteString (NonEmpty SolSignature) +type SignatureMap = Map ByteString (NonEmpty SolSignature) getBytecodeMetadata :: ByteString -> ByteString getBytecodeMetadata bs = @@ -54,13 +54,13 @@ makeBytecodeCache :: [ByteString] -> MetadataCache makeBytecodeCache bss = M.fromList $ bss `zip` (getBytecodeMetadata <$> bss) knownBzzrPrefixes :: [ByteString] -knownBzzrPrefixes = [ +knownBzzrPrefixes = -- a1 65 "bzzr0" 0x58 0x20 (solc <= 0.5.8) - BS.pack [0xa1, 0x65, 98, 122, 122, 114, 48, 0x58, 0x20], + [ BS.pack [0xa1, 0x65, 98, 122, 122, 114, 48, 0x58, 0x20] -- a2 65 "bzzr0" 0x58 0x20 (solc >= 0.5.9) - BS.pack [0xa2, 0x65, 98, 122, 122, 114, 48, 0x58, 0x20], + , BS.pack [0xa2, 0x65, 98, 122, 122, 114, 48, 0x58, 0x20] -- a2 65 "bzzr1" 0x58 0x20 (solc >= 0.5.11) - BS.pack [0xa2, 0x65, 98, 122, 122, 114, 49, 0x58, 0x20], + , BS.pack [0xa2, 0x65, 98, 122, 122, 114, 49, 0x58, 0x20] -- a2 64 "ipfs" 0x58 0x22 (solc >= 0.6.0) - BS.pack [0xa2, 0x64, 0x69, 0x70, 0x66, 0x73, 0x58, 0x22] + , BS.pack [0xa2, 0x64, 0x69, 0x70, 0x66, 0x73, 0x58, 0x22] ] diff --git a/lib/Echidna/Types/Solidity.hs b/lib/Echidna/Types/Solidity.hs index d48f99e27..a6b4b65bd 100644 --- a/lib/Echidna/Types/Solidity.hs +++ b/lib/Echidna/Types/Solidity.hs @@ -15,24 +15,25 @@ minSupportedSolcVersion = version 0 4 25 [] [] data Filter = Blacklist [Text] | Whitelist [Text] deriving Show --- | Things that can go wrong trying to load a Solidity file for Echidna testing. Read the 'Show' --- instance for more detailed explanations. -data SolException = BadAddr Addr - | CompileFailure String String - | SolcReadFailure - | NoContracts - | TestArgsFound Text - | ContractNotFound Text - | NoBytecode Text - | NoFuncs - | NoTests - | OnlyTests - | ConstructorArgs String - | DeploymentFailed Addr Text - | SetUpCallFailed - | NoCryticCompile - | InvalidMethodFilters Filter - | OutdatedSolcVersion Version +-- | Things that can go wrong trying to load a Solidity file for Echidna testing. +-- Read the 'Show' instance for more detailed explanations. +data SolException + = BadAddr Addr + | CompileFailure String String + | SolcReadFailure + | NoContracts + | TestArgsFound Text + | ContractNotFound Text + | NoBytecode Text + | NoFuncs + | NoTests + | OnlyTests + | ConstructorArgs String + | DeploymentFailed Addr Text + | SetUpCallFailed + | NoCryticCompile + | InvalidMethodFilters Filter + | OutdatedSolcVersion Version instance Show SolException where show = \case diff --git a/lib/Echidna/Types/Tx.hs b/lib/Echidna/Types/Tx.hs index 37cbfca4a..4f902d36f 100644 --- a/lib/Echidna/Types/Tx.hs +++ b/lib/Echidna/Types/Tx.hs @@ -29,10 +29,11 @@ import Data.DoubleWord (Word256, Word128, Int256, Int128, Word160) -- | A transaction call is either a @CREATE@, a fully instrumented 'SolCall', or -- an abstract call consisting only of calldata. -data TxCall = SolCreate !ByteString - | SolCall !SolCall - | SolCalldata !ByteString - | NoCall +data TxCall + = SolCreate !ByteString + | SolCall !SolCall + | SolCalldata !ByteString + | NoCall deriving (Show, Ord, Eq, Generic) $(deriveJSON defaultOptions ''TxCall) @@ -56,14 +57,15 @@ initialBlockNumber = 4370000 -- Initial byzantium block -- | A transaction is either a @CREATE@ or a regular call with an origin, destination, and value. -- Note: I currently don't model nonces or signatures here. -data Tx = Tx { call :: !TxCall -- | Call - , src :: !Addr -- | Origin - , dst :: !Addr -- | Destination - , gas :: !Word64 -- | Gas - , gasprice :: !W256 -- | Gas price - , value :: !W256 -- | Value - , delay :: !(W256, W256) -- | (Time, # of blocks since last call) - } deriving (Eq, Ord, Show, Generic) +data Tx = Tx + { call :: !TxCall -- ^ Call + , src :: !Addr -- ^ Origin + , dst :: !Addr -- ^ Destination + , gas :: !Word64 -- ^ Gas + , gasprice :: !W256 -- ^ Gas price + , value :: !W256 -- ^ Value + , delay :: !(W256, W256) -- ^ (Time, # of blocks since last call) + } deriving (Eq, Ord, Show, Generic) deriving instance NFData Tx deriving instance NFData TxCall @@ -121,77 +123,82 @@ basicTx :: Text -- | Function name -> Tx basicTx f a s d g = basicTxWithValue f a s d g 0 -basicTxWithValue :: Text -- | Function name - -> [AbiValue] -- | Function args - -> Addr -- | Sender - -> Addr -- | Destination contract - -> Word64 -- | Gas limit - -> W256 -- | Value - -> (W256, W256) -- | Block increment - -> Tx +basicTxWithValue + :: Text -- ^ Function name + -> [AbiValue] -- ^ Function args + -> Addr -- ^ Sender + -> Addr -- ^ Destination contract + -> Word64 -- ^ Gas limit + -> W256 -- ^ Value + -> (W256, W256) -- ^ Block increment + -> Tx basicTxWithValue f a s d g = Tx (SolCall (f, a)) s d g 0 -createTx :: ByteString -- | Constructor bytecode - -> Addr -- | Creator - -> Addr -- | Destination address - -> Word64 -- | Gas limit - -> (W256, W256) -- | Block increment - -> Tx +createTx + :: ByteString -- ^ Constructor bytecode + -> Addr -- ^ Creator + -> Addr -- ^ Destination address + -> Word64 -- ^ Gas limit + -> (W256, W256) -- ^ Block increment + -> Tx createTx bc s d g = createTxWithValue bc s d g 0 -createTxWithValue :: ByteString -- | Constructor bytecode - -> Addr -- | Creator - -> Addr -- | Destination address - -> Word64 -- | Gas limit - -> W256 -- | Value - -> (W256, W256) -- | Block increment - -> Tx +createTxWithValue + :: ByteString -- ^ Constructor bytecode + -> Addr -- ^ Creator + -> Addr -- ^ Destination address + -> Word64 -- ^ Gas limit + -> W256 -- ^ Value + -> (W256, W256) -- ^ Block increment + -> Tx createTxWithValue bc s d g = Tx (SolCreate bc) s d g 0 -data TxResult = ReturnTrue - | ReturnFalse - | Stop - | ErrorBalanceTooLow - | ErrorUnrecognizedOpcode - | ErrorSelfDestruction - | ErrorStackUnderrun - | ErrorBadJumpDestination - | ErrorRevert - | ErrorOutOfGas - | ErrorBadCheatCode - | ErrorStackLimitExceeded - | ErrorIllegalOverflow - | ErrorQuery - | ErrorStateChangeWhileStatic - | ErrorInvalidFormat - | ErrorInvalidMemoryAccess - | ErrorCallDepthLimitReached - | ErrorMaxCodeSizeExceeded - | ErrorPrecompileFailure - | ErrorUnexpectedSymbolic - | ErrorDeadPath - | ErrorChoose -- not entirely sure what this is - | ErrorWhiffNotUnique - | ErrorSMTTimeout - | ErrorFFI - | ErrorNonceOverflow - | ErrorReturnDataOutOfBounds +data TxResult + = ReturnTrue + | ReturnFalse + | Stop + | ErrorBalanceTooLow + | ErrorUnrecognizedOpcode + | ErrorSelfDestruction + | ErrorStackUnderrun + | ErrorBadJumpDestination + | ErrorRevert + | ErrorOutOfGas + | ErrorBadCheatCode + | ErrorStackLimitExceeded + | ErrorIllegalOverflow + | ErrorQuery + | ErrorStateChangeWhileStatic + | ErrorInvalidFormat + | ErrorInvalidMemoryAccess + | ErrorCallDepthLimitReached + | ErrorMaxCodeSizeExceeded + | ErrorPrecompileFailure + | ErrorUnexpectedSymbolic + | ErrorDeadPath + | ErrorChoose -- not entirely sure what this is + | ErrorWhiffNotUnique + | ErrorSMTTimeout + | ErrorFFI + | ErrorNonceOverflow + | ErrorReturnDataOutOfBounds deriving (Eq, Ord, Show) $(deriveJSON defaultOptions ''TxResult) -data TxConf = TxConf { propGas :: Word64 - -- ^ Gas to use evaluating echidna properties - , txGas :: Word64 - -- ^ Gas to use in generated transactions - , maxGasprice :: W256 - -- ^ Maximum gasprice to be checked for a transaction - , maxTimeDelay :: W256 - -- ^ Maximum time delay between transactions (seconds) - , maxBlockDelay :: W256 - -- ^ Maximum block delay between transactions - , maxValue :: W256 - -- ^ Maximum value to use in transactions - } +data TxConf = TxConf + { propGas :: Word64 + -- ^ Gas to use evaluating echidna properties + , txGas :: Word64 + -- ^ Gas to use in generated transactions + , maxGasprice :: W256 + -- ^ Maximum gasprice to be checked for a transaction + , maxTimeDelay :: W256 + -- ^ Maximum time delay between transactions (seconds) + , maxBlockDelay :: W256 + -- ^ Maximum block delay between transactions + , maxValue :: W256 + -- ^ Maximum value to use in transactions + } -- | Transform a VMResult into a more hash friendly sum type getResult :: VMResult -> TxResult diff --git a/lib/Echidna/Types/World.hs b/lib/Echidna/Types/World.hs index 86ed03155..bc9fad11c 100644 --- a/lib/Echidna/Types/World.hs +++ b/lib/Echidna/Types/World.hs @@ -1,19 +1,21 @@ module Echidna.Types.World where +import Data.Set (Set) + import EVM.Types (Addr) import Echidna.Types.Signature (FunctionHash, SignatureMap) import Echidna.Events (EventMap) -import Data.Set (Set) -- | The world is composed by: -- * A list of "human" addresses -- * A high-priority map of signatures from every contract -- * A low-priority map of signatures from every contract -- * A list of function hashes from payable functions -data World = World { senders :: Set Addr - , highSignatureMap :: SignatureMap - , lowSignatureMap :: Maybe SignatureMap - , payableSigs :: [FunctionHash] - , eventMap :: EventMap - } +data World = World + { senders :: Set Addr + , highSignatureMap :: SignatureMap + , lowSignatureMap :: Maybe SignatureMap + , payableSigs :: [FunctionHash] + , eventMap :: EventMap + } diff --git a/lib/Echidna/UI.hs b/lib/Echidna/UI.hs index a1fb16b42..ce81573ab 100644 --- a/lib/Echidna/UI.hs +++ b/lib/Echidna/UI.hs @@ -9,8 +9,8 @@ import Brick.Widgets.Dialog qualified as B import Control.Monad.Catch (MonadCatch(..), catchAll) import Control.Monad.Reader (MonadReader (ask), runReader, asks) import Control.Monad.State (modify') -import Graphics.Vty qualified as V import Graphics.Vty (Config, Event(..), Key(..), Modifier(..), defaultConfig, inputMap, mkVty) +import Graphics.Vty qualified as Vty import System.Posix import Echidna.UI.Widgets @@ -56,32 +56,33 @@ data UIEvent = -- | Set up and run an Echidna 'Campaign' and display interactive UI or -- print non-interactive output in desired format at the end -ui :: (MonadCatch m, MonadRandom m, MonadReader Env m, MonadUnliftIO m) - => VM -- ^ Initial VM state - -> World -- ^ Initial world state - -> [EchidnaTest] -- ^ Tests to evaluate - -> GenDict - -> [[Tx]] - -> m Campaign +ui + :: (MonadCatch m, MonadRandom m, MonadReader Env m, MonadUnliftIO m) + => VM -- ^ Initial VM state + -> World -- ^ Initial world state + -> [EchidnaTest] -- ^ Tests to evaluate + -> GenDict + -> [[Tx]] + -> m Campaign ui vm world ts dict initialCorpus = do conf <- asks (.cfg) - let uiConf = conf.uiConf ref <- liftIO $ newIORef defaultCampaign stop <- newEmptyMVar - let updateRef = do - shouldStop <- liftIO $ isJust <$> tryReadMVar stop - get >>= liftIO . atomicWriteIORef ref - pure shouldStop - - secToUsec = (* 1000000) - timeoutUsec = secToUsec $ fromMaybe (-1) uiConf.maxTime - runCampaign = timeout timeoutUsec (campaign updateRef vm world ts dict initialCorpus) + let + updateRef = do + shouldStop <- liftIO $ isJust <$> tryReadMVar stop + get >>= liftIO . atomicWriteIORef ref + pure shouldStop + + secToUsec = (* 1000000) + timeoutUsec = secToUsec $ fromMaybe (-1) conf.uiConf.maxTime + runCampaign = timeout timeoutUsec (campaign updateRef vm world ts dict initialCorpus) #ifdef INTERACTIVE_UI terminalPresent <- liftIO isTerminal #else let terminalPresent = False #endif - let effectiveMode = case uiConf.operationMode of + let effectiveMode = case conf.uiConf.operationMode of Interactive | not terminalPresent -> NonInteractive Text other -> other case effectiveMode of @@ -109,7 +110,7 @@ ui vm world ts dict initialCorpus = do (const $ liftIO $ killThread ticker) let buildVty = do v <- mkVty =<< vtyConfig - V.setMode (V.outputIface v) V.Mouse True + Vty.setMode (Vty.outputIface v) Vty.Mouse True pure v initialVty <- liftIO buildVty app <- customMain initialVty buildVty (Just bc) <$> monitor @@ -163,11 +164,10 @@ ui vm world ts dict initialCorpus = do vtyConfig :: IO Config vtyConfig = do - config <- V.standardIOConfig + config <- Vty.standardIOConfig pure config { inputMap = (Nothing, "\ESC[6;2~", EvKey KPageDown [MShift]) : (Nothing, "\ESC[5;2~", EvKey KPageUp [MShift]) : - inputMap defaultConfig - } + inputMap defaultConfig } -- | Check if we should stop drawing (or updating) the dashboard, then do the right thing. monitor :: MonadReader Env m => m (App UIState UIEvent Name) @@ -178,7 +178,7 @@ monitor = do [ if uiState.displayFetchedDialog then fetchedDialogWidget uiState else emptyWidget - , runReader (campaignStatus uiState) conf] + , runReader (campaignStatus uiState) conf ] onEvent (AppEvent (CampaignUpdated c')) = modify' $ \state -> state { campaign = c', status = Running } diff --git a/lib/Echidna/Utility.hs b/lib/Echidna/Utility.hs index 4300a1936..a93487810 100644 --- a/lib/Echidna/Utility.hs +++ b/lib/Echidna/Utility.hs @@ -1,9 +1,11 @@ module Echidna.Utility where import Control.Monad (unless) +import Control.Monad.Catch (bracket) import Data.Time (diffUTCTime, getCurrentTime) -import Data.Time.Format -import Data.Time.LocalTime +import Data.Time.Format (defaultTimeLocale, formatTime) +import Data.Time.LocalTime (utcToLocalZonedTime) +import System.Directory (getDirectoryContents, getCurrentDirectory, setCurrentDirectory) import System.IO (hFlush, stdout) measureIO :: Bool -> String -> IO b -> IO b @@ -21,3 +23,16 @@ timePrefix :: IO String timePrefix = do time <- utcToLocalZonedTime =<< getCurrentTime pure $ "[" <> formatTime defaultTimeLocale "%F %T.%2q" time <> "] " + +listDirectory :: FilePath -> IO [FilePath] +listDirectory path = filter f <$> getDirectoryContents path + where f filename = filename /= "." && filename /= ".." + +withCurrentDirectory + :: FilePath -- ^ Directory to execute in + -> IO a -- ^ Action to be executed + -> IO a +withCurrentDirectory dir action = + bracket getCurrentDirectory setCurrentDirectory $ \_ -> do + setCurrentDirectory dir + action diff --git a/package.yaml b/package.yaml index 9ad8397c2..09e89a169 100644 --- a/package.yaml +++ b/package.yaml @@ -41,9 +41,7 @@ dependencies: - time - unliftio - utf8-string - - unordered-containers - vector - - vector-instances - with-utf8 - word-wrap - yaml