Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into dev-power-gen
Browse files Browse the repository at this point in the history
  • Loading branch information
ggrieco-tob committed Jan 24, 2023
2 parents 25107c5 + 0010323 commit 6ca5396
Show file tree
Hide file tree
Showing 37 changed files with 890 additions and 931 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
apt-get: autoconf automake libtool
- os: macos-latest
brew: automake

steps:
- name: Get Packages
uses: mstksg/get-package@v1
Expand Down Expand Up @@ -64,7 +64,7 @@ jobs:
- name: Build and install echidna
run: |
stack install --ghc-options="-Werror" --extra-include-dirs=$HOME/.local/include --extra-lib-dirs=$HOME/.local/lib
stack install --flag echidna:static --ghc-options="-Werror" --extra-include-dirs=$HOME/.local/include --extra-lib-dirs=$HOME/.local/lib
- name: Amend and compress binaries (macOS)
if: runner.os == 'macOS'
Expand All @@ -83,7 +83,7 @@ jobs:
- name: Build and copy test suite
if: runner.os == 'Linux'
run: |
stack build --test --no-run-tests --ghc-options="-Werror" --extra-include-dirs=$HOME/.local/include --extra-lib-dirs=$HOME/.local/lib
stack build --flag echidna:static --test --no-run-tests --ghc-options="-Werror" --extra-include-dirs=$HOME/.local/include --extra-lib-dirs=$HOME/.local/lib
cp "$(find "$PWD" -name echidna-testsuite -type f)" .
- name: Upload testsuite
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
## TODO

## 2.0.5

* Optimized constant generation (#898, #900)
* Fixed how address are displayed in events (#891)
* Update hevm to 0.50 (#884, #894, #896, #897, #901)
* Added saving and loading of reproducers for every test (#858)
* Added events and revert reasons for any failure in the constructor (#871)
* Fixed uninitialized sender addresses from etheno transactions (#823)
* Fixed crash when minimizing inputs during optimization tests (#837)
* Refactored code and removed useless dependencies (#856, #857, #874, #878, #895, #903)

## 2.0.4

* Added colored html for coverage output code (#816)
Expand Down
4 changes: 2 additions & 2 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ RUN curl -sSL https://get.haskellstack.org/ | sh
COPY . /echidna/
WORKDIR /echidna
RUN .github/scripts/install-libff.sh
RUN stack upgrade && stack setup && stack install --extra-include-dirs=/usr/local/include --extra-lib-dirs=/usr/local/lib
RUN stack upgrade && stack setup && stack install --flag echidna:static --extra-include-dirs=/usr/local/include --extra-lib-dirs=/usr/local/lib


FROM ubuntu:focal AS builder-python3
Expand All @@ -28,7 +28,7 @@ RUN apt-get update && \
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
ENV PIP_NO_CACHE_DIR=1
RUN python3 -m venv /venv && /venv/bin/pip3 install --no-cache --upgrade setuptools pip
RUN /venv/bin/pip3 install --no-cache slither-analyzer solc-select
RUN /venv/bin/pip3 install --no-cache slither-analyzer solc-select "crytic-compile @ https://github.com/crytic/crytic-compile/archive/53167f3f3d63b73916b1660312a53fd952f2e3dd.zip"


FROM gcr.io/distroless/python3-debian11:nonroot AS final-distroless
Expand Down
15 changes: 9 additions & 6 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
{
nixConfig = {
extra-substituters = "https://trailofbits.cachix.org";
extra-trusted-public-keys = "trailofbits.cachix.org-1:jRuxrlFghP6HstIaZg7DhvTgHyK/lcYa7U8y3CgKjzU=";
};

inputs = {
nixpkgs.url = "github:NixOS/nixpkgs";
flake-utils.url = "github:numtide/flake-utils";
Expand Down Expand Up @@ -43,8 +38,16 @@
'';
};

hevm = pkgs.haskell.lib.dontCheck (
pkgs.haskellPackages.callCabal2nix "hevm" (pkgs.fetchFromGitHub {
owner = "ethereum";
repo = "hevm";
rev = "2f498916e8d46966baa17472b16e5894b1adfc06";
sha256 = "sha256-i+4vIA/z+iuR79bqXOW8zw/pQZGAVjs9nsukd1WEBlc=";
}) { secp256k1 = pkgs.secp256k1; });

echidna = with pkgs; lib.pipe
(haskellPackages.callCabal2nix "echidna" ./. { })
(haskellPackages.callCabal2nix "echidna" ./. { inherit hevm; })
[
(haskell.lib.compose.addTestToolDepends [ haskellPackages.hpack slither-analyzer solc ])
(haskell.lib.compose.disableCabalFlag "static")
Expand Down
75 changes: 43 additions & 32 deletions lib/Echidna.hs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
module Echidna where

import Control.Lens (view, (^.), to)
import Data.Has (Has(..))
import Control.Monad.Catch (MonadCatch(..), MonadThrow(..))
import Control.Monad.Reader (MonadReader, MonadIO, liftIO)
import Data.HashMap.Strict (toList)
import Data.Map.Strict (keys)
import Data.List (nub, find)
import Control.Monad.Catch (MonadThrow(..))
import Data.HashMap.Strict qualified as HM
import Data.List (find)
import Data.List.NonEmpty qualified as NE
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import System.FilePath ((</>))

import EVM (env, contracts, VM)
import EVM
import EVM.ABI (AbiValue(AbiAddress))
import EVM.Solidity (SourceCache, SolcContract)

Expand Down Expand Up @@ -37,44 +36,56 @@ import Echidna.RPC (loadEtheno, extractFromEtheno)
-- * A VM with the contract deployed and ready for testing
-- * A World with all the required data for generating random transctions
-- * A list of Echidna tests to check
-- * A prepopulated dictionary (if any)
-- * A prepopulated dictionary
-- * A list of transaction sequences to initialize the corpus
prepareContract :: (MonadCatch m, MonadReader x m, MonadIO m, MonadFail m, Has SolConf x)
=> EConfig -> NE.NonEmpty FilePath -> Maybe ContractName -> Seed
-> m (VM, SourceCache, [SolcContract], World, [EchidnaTest], Maybe GenDict, [[Tx]])
prepareContract :: EConfig -> NE.NonEmpty FilePath -> Maybe ContractName -> Seed
-> IO (VM, SourceCache, [SolcContract], World, [EchidnaTest], GenDict, [[Tx]])
prepareContract cfg fs c g = do
ctxs1 <- liftIO $ loadTxs (fmap (++ "/reproducers/") cd)
ctxs2 <- liftIO $ loadTxs (fmap (++ "/coverage/") cd)
let ctxs = ctxs1 ++ ctxs2
ctxs <- case cfg._cConf._corpusDir of
Nothing -> pure []
Just dir -> do
ctxs1 <- loadTxs (dir </> "reproducers")
ctxs2 <- loadTxs (dir </> "coverage")
pure (ctxs1 ++ ctxs2)

let solConf = cfg._sConf

-- compile and load contracts
(cs, scs) <- Echidna.Solidity.contracts fs
p <- loadSpecified c cs
(cs, scs) <- Echidna.Solidity.contracts solConf fs
p <- loadSpecified solConf c cs

-- run processors
ca <- view (hasLens . cryticArgs)
si <- runSlither (NE.head fs) ca
case find (< minSupportedSolcVersion) $ solcVersions si of
si <- runSlither (NE.head fs) solConf._cryticArgs
case find (< minSupportedSolcVersion) si.solcVersions of
Just outdatedVersion -> throwM $ OutdatedSolcVersion outdatedVersion
Nothing -> return ()

-- load tests
(v, w, ts) <- prepareForTest p c si
let (vm, world, ts) = prepareForTest solConf p c si

-- get signatures
let sigs = nub $ concatMap (NE.toList . snd) (toList $ w ^. highSignatureMap)
let sigs = Set.fromList $ concatMap NE.toList (HM.elems world.highSignatureMap)

ads <- addresses
let ads' = AbiAddress <$> v ^. env . EVM.contracts . to keys
let constants' = enhanceConstants si ++ timeConstants ++ extremeConstants ++ NE.toList ads ++ ads'
let ads = addresses solConf
let ads' = AbiAddress <$> Map.keys vm._env._contracts
let constants' = Set.fromList $ enhanceConstants si ++
timeConstants ++
extremeConstants ++
Set.toList ads ++
ads'

-- load transactions from init sequence (if any)
es' <- liftIO $ maybe (return []) loadEtheno it
let txs = ctxs ++ maybe [] (const [extractFromEtheno es' sigs]) it
ethenoCorpus <-
case cfg._sConf._initialize of
Nothing -> pure []
Just fp -> do
es' <- loadEtheno fp
pure [extractFromEtheno es' sigs]

let corp = ctxs ++ ethenoCorpus

-- start ui and run tests
let sc = selectSourceCache c scs
return (v, sc, cs, w, ts, Just $ mkGenDict df constants' [] g (returnTypes cs), txs)
where cd = cfg ^. cConf . corpusDir
df = cfg ^. cConf . dictFreq
it = cfg ^. sConf . initialize

let dict = mkGenDict cfg._cConf._dictFreq constants' Set.empty g (returnTypes cs)

pure (vm, sc, cs, world, ts, dict, corp)
50 changes: 25 additions & 25 deletions lib/Echidna/ABI.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ module Echidna.ABI where

import Control.Lens
import Control.Monad (join, liftM2, liftM3, foldM, replicateM)
import Control.Monad.Random.Strict (MonadRandom, getRandom, getRandoms, getRandomR, uniformMay)
import Control.Monad.Random.Strict (MonadRandom, getRandom, getRandoms, getRandomR)
import Control.Monad.Random.Strict qualified as R
import Data.Binary.Put (runPut, putWord32be)
import Data.BinaryWord (unsignedWord)
import Data.Bits (bit)
import Data.Bool (bool)
import Data.ByteString.Lazy as BSLazy (toStrict)
import Data.ByteString (ByteString)
Expand All @@ -18,12 +20,11 @@ import Data.Foldable (toList)
import Data.Hashable (Hashable(..))
import Data.HashMap.Strict (HashMap)
import Data.HashMap.Strict qualified as M
import Data.HashSet (HashSet, fromList, union)
import Data.Set (Set)
import Data.Set qualified as Set
import Data.List (intercalate)
import Data.List.NonEmpty qualified as NE
import Data.Maybe (fromMaybe, catMaybes, mapMaybe)
import Data.Maybe (fromMaybe, catMaybes)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Encoding (encodeUtf8)
Expand All @@ -49,10 +50,11 @@ commonTypeSizes :: [Int]
commonTypeSizes = [8,16..256]

mkValidAbiInt :: Int -> Int256 -> Maybe AbiValue
mkValidAbiInt i x = if abs x <= 2 ^ (i - 1) - 1 then Just $ AbiInt i x else Nothing
mkValidAbiInt i x = if unsignedWord (abs x) < bit (i - 1) then Just $ AbiInt i x else Nothing

mkValidAbiUInt :: Int -> Word256 -> Maybe AbiValue
mkValidAbiUInt i x = if x <= 2 ^ i - 1 then Just $ AbiUInt i x else Nothing
mkValidAbiUInt 256 x = Just $ AbiUInt 256 x
mkValidAbiUInt i x = if x < bit i then Just $ AbiUInt i x else Nothing

makeNumAbiValues :: Integer -> [AbiValue]
makeNumAbiValues i = let l f = f <$> commonTypeSizes <*> fmap fromIntegral ([i-1..i+1] ++ [(-i)-1 .. (-i)+1]) in
Expand Down Expand Up @@ -101,9 +103,9 @@ hashSig = abiKeccak . TE.encodeUtf8
-- | Configuration necessary for generating new 'SolCalls'. Don't construct this by hand! Use 'mkConf'.
data GenDict = GenDict { _pSynthA :: Float
-- ^ Fraction of time to use dictionary vs. synthesize
, _constants :: HashMap AbiType (HashSet AbiValue)
, _constants :: HashMap AbiType (Set AbiValue)
-- ^ Constants to use, sorted by type
, _wholeCalls :: HashMap SolSignature (HashSet SolCall)
, _wholeCalls :: HashMap SolSignature (Set SolCall)
-- ^ Whole calls to use, sorted by type
, _defSeed :: Int
-- ^ Default seed to use if one is not provided in EConfig
Expand All @@ -115,14 +117,14 @@ data GenDict = GenDict { _pSynthA :: Float

makeLenses 'GenDict

hashMapBy :: (Hashable k, Hashable a, Eq k, Ord a) => (a -> k) -> [a] -> HashMap k (HashSet a)
hashMapBy f = M.fromListWith union . fmap (\v -> (f v, fromList [v]))
hashMapBy :: (Hashable k, Hashable a, 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

gaddCalls :: [SolCall] -> GenDict -> GenDict
gaddCalls :: Set SolCall -> GenDict -> GenDict
gaddCalls c = wholeCalls <>~ hashMapBy (fmap $ fmap abiValueType) c

defaultDict :: GenDict
defaultDict = mkGenDict 0 [] [] 0 (const Nothing)
defaultDict = mkGenDict 0 Set.empty Set.empty 0 (const Nothing)

deriving anyclass instance Hashable AbiType
deriving anyclass instance Hashable AbiValue
Expand All @@ -131,17 +133,17 @@ deriving anyclass instance Hashable Addr
-- | Construct a 'GenDict' from some dictionaries, a 'Float', a default seed, and a typing rule for
-- return values
mkGenDict :: Float -- ^ Percentage of time to mutate instead of synthesize. Should be in [0,1]
-> [AbiValue] -- ^ A list of 'AbiValue' constants to use during dictionary-based generation
-> [SolCall] -- ^ A list of complete 'SolCall's to mutate
-> Set AbiValue -- ^ A list of 'AbiValue' constants to use during dictionary-based generation
-> Set SolCall -- ^ A list of complete 'SolCall's to mutate
-> Int -- ^ A default seed
-> (Text -> Maybe AbiType)
-- ^ A return value typing rule
-> GenDict
mkGenDict p vs cs s tr =
GenDict p (hashMapBy abiValueType vs) (hashMapBy (fmap $ fmap abiValueType) cs) s tr (mkDictValues vs)

mkDictValues :: [AbiValue] -> Set W256
mkDictValues vs = Set.fromList $ mapMaybe fromValue vs
mkDictValues :: Set AbiValue -> Set W256
mkDictValues = Set.foldl' (\acc e -> maybe acc (`Set.insert` acc) (fromValue e)) Set.empty
where fromValue (AbiUInt _ n) = Just (fromIntegral n)
fromValue (AbiInt _ n) = Just (fromIntegral n)
fromValue _ = Nothing
Expand Down Expand Up @@ -170,10 +172,6 @@ genAbiValue = genAbiValueM defaultDict
genAbiCall :: MonadRandom m => SolSignature -> m SolCall
genAbiCall = traverse $ traverse genAbiValue

-- | Synthesize a random 'SolCall' given a list of 'SolSignature's (effectively, an ABI). Doesn't use a dictionary.
genInteractions :: MonadRandom m => NE.NonEmpty SolSignature -> m SolCall
genInteractions l = genAbiCall =<< rElem l

-- Mutation helper functions

-- | Given an 'Integral' number n, get a random number in [0,2n].
Expand Down Expand Up @@ -255,7 +253,7 @@ shrinkAbiValue :: MonadRandom m => AbiValue -> m AbiValue
shrinkAbiValue (AbiUInt n m) = AbiUInt n <$> shrinkInt m
shrinkAbiValue (AbiInt n m) = AbiInt n <$> shrinkInt m
shrinkAbiValue (AbiAddress 0) = pure $ AbiAddress 0
shrinkAbiValue (AbiAddress _) = rElem $ NE.fromList [AbiAddress 0, AbiAddress 0xdeadbeef]
shrinkAbiValue (AbiAddress _) = rElem' $ Set.fromList [AbiAddress 0, AbiAddress 0xdeadbeef]
shrinkAbiValue (AbiBool _) = pure $ AbiBool False
shrinkAbiValue (AbiBytes n b) = AbiBytes n <$> addNulls b
shrinkAbiValue (AbiBytesDynamic b) = fmap AbiBytesDynamic $ addNulls =<< shrinkBS b
Expand Down Expand Up @@ -312,16 +310,18 @@ mutateAbiCall = traverse f
-- @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 [b] -> (a -> m b) -> a -> m b
=> GenDict -> HashMap 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 = uniformMay (M.lookupDefault [] t m)
let maybeValM = if genDict._pSynthA >= r then fromDict else pure Nothing
fromDict = case M.lookup t m of
Nothing -> pure Nothing
Just cs -> Just <$> rElem' cs
fromMaybe <$> g t <*> maybeValM

-- | Synthesize a random 'AbiValue' given its 'AbiType'. Requires a dictionary.
genAbiValueM :: MonadRandom m => GenDict -> AbiType -> m AbiValue
genAbiValueM genDict = genWithDict genDict (toList <$> genDict ^. constants) $ \case
genAbiValueM genDict = genWithDict genDict genDict._constants $ \case
(AbiUIntType n) -> fixAbiUInt n . fromInteger <$> getRandomUint n
(AbiIntType n) -> fixAbiInt n . fromInteger <$> getRandomInt n
AbiAddressType -> AbiAddress . fromInteger <$> getRandomR (0, 2 ^ (160 :: Integer) - 1)
Expand All @@ -340,7 +340,7 @@ genAbiValueM genDict = genWithDict genDict (toList <$> genDict ^. constants) $ \
genAbiCallM :: MonadRandom m => GenDict -> SolSignature -> m SolCall
genAbiCallM genDict abi = do
solCall <- genWithDict genDict
(toList <$> genDict ^. wholeCalls)
genDict._wholeCalls
(traverse $ traverse (genAbiValueM genDict))
abi
mutateAbiCall solCall
Expand Down
Loading

0 comments on commit 6ca5396

Please sign in to comment.