diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 5f73d44b6..624a7e703 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -61,7 +61,7 @@ jobs: password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }} - name: Docker Build and Push (Ubuntu & NVM variant) - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: platforms: linux/amd64 target: final-ubuntu @@ -74,7 +74,7 @@ jobs: cache-to: type=gha,mode=max - name: Docker Build and Push (Distroless variant) - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 if: ${{ env.WORKFLOW_BUILD_DISTROLESS == true }} with: platforms: linux/amd64 diff --git a/CHANGELOG.md b/CHANGELOG.md index f42c9559a..b6d7fd0ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ -## TODO +## 2.1.0 (Unreleased) + +* Rename multi-abi mode to allContracts ## 2.0.5 @@ -7,26 +9,26 @@ * 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) +* 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) -* Fixed crash when parsing solc versions (#835) +* Fixed crash when parsing solc versions (#835) * Fixed long transactions and event lines in UI (#832) -* Added Homebrew installation instructions (#848) +* Added Homebrew installation instructions (#848) * Moved all nix stuff to flake and use nix-bundle-exe for macOS release (#851) -* Updated codebase to GHC 9.0.2 (#846) +* Updated codebase to GHC 9.0.2 (#846) * Refactored code and removed useless dependencies (#854, #853, #829, #827, #828) ## 2.0.3 * Clean up Docker containers (#706) * Avoid resetting accounts if there is a deployed contract (#795) -* Fixed decoding non-utf8 strings from slither printer (#799) -* Fixed generation and mutation of extreme signed integers (#791) +* Fixed decoding non-utf8 strings from slither printer (#799) +* Fixed generation and mutation of extreme signed integers (#791) * Removed fallback from signature map when it is not defined (#772) ## 2.0.2 @@ -37,13 +39,13 @@ * Fixed crash when the EVM execution triggers more than one query (#760) * Added support for detection and handling of ancient solc versions (#675) * Added explicit static flag and removed pthread one from ghc options (#768) - + ## 2.0.1 * Optimized stateless mutators (#747) * Expanded and improved command-line help (#741) * Added dapptest support: compatibility mode to run foundry and dapptool fuzz tests (#733, #745) -* Generate more values closer to the maximum (#736) +* Generate more values closer to the maximum (#736) * Fix TERMINFO path for Nix release builds (#731) * Mitigate large memory consumption when replaying corpus (#725) * Fix --shrink-limit to change shrink limit instead of test limit (#728) @@ -54,14 +56,14 @@ * Refactored test internal data structures and code * Refactored unit test code and moved the related files to the `tests` directory -* Added support to show events and custom errors when a property/assertion fails +* Added support to show events and custom errors when a property/assertion fails * Added support for catching assertion failure in Solidity 0.8.x * Added two new testing mode: optimization and overflow (only in Solidity 0.8.x) * Added optional checks for contract destruction * Added `testMode` option and removed related flags * Simplified contract deployer and property sender addresses to be easier to read * Updated hevm to 0.49.0 - + ## 1.7.3 * Removed old compilation artifacts before starting a new fuzzing campaign (#697) @@ -92,17 +94,17 @@ * Refactor coverage types and added corpus size in UI (#627) * Fixed link to macOS binary in binaries.soliditylang.org (#629) * Fixed default.nix to use 1.7.0 as version (#623) -* Refactored Test type (#622) +* Refactored Test type (#622) ## 1.7.0 -* Refactored and improved etheno support to be more useful (#615) +* Refactored and improved etheno support to be more useful (#615) * Coverage filenames are not overwritten (#620) * Refactored the mutator code (#618) * More corpus and array mutations implemented (#372) * Source coverage is printed after fuzzing campaign (#516) * Nix improvements and fixes (#603, #604, #608, #612) -* Simplified slither information parsing (#543) +* Simplified slither information parsing (#543) * Enabled use of coverage by default (#605) * Run echidna tests in parallel (#571) @@ -112,10 +114,10 @@ * Use metadata to detect deployed contracts (#593) * Semver integration for improving testing with different solc versions (#594) * Added some performance improvement in property execution (#576) -* Fixed wait bug when shrinking (#584) +* Fixed wait bug when shrinking (#584) * Added funwithnumber example from Sabre (#565) * Improved function filtering to be more precise (#570) -* Small fixes in the macOS CI (#597), the README (#590) and Nix (#581) +* Small fixes in the macOS CI (#597), the README (#590) and Nix (#581) ## 1.6.0 * Slither is now a required dependency. @@ -134,7 +136,7 @@ * Fix negative address bug (#552) * Various Github Actions improvements (#527, #554) * Allow to bypass EIP-170 and set up a custom max code size (#544) - + ## 1.5.1 * Fix timestamp and block delays having the initial timestamp/block added to them (#460, #469) diff --git a/README.md b/README.md index 55cc0f882..6ed47ce4e 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ Our tool signals each execution trace in the corpus with the following "line mar Echidna can test contracts compiled with different smart contract build systems, including [Truffle](https://truffleframework.com/) or [hardhat](https://hardhat.org/) using [crytic-compile](https://github.com/crytic/crytic-compile). To invoke echidna with the current compilation framework, use `echidna .`. -On top of that, Echidna supports two modes of testing complex contracts. Firstly, one can [describe an initialization procedure with Truffle and Etheno](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/end-to-end-testing.md) and use that as the base state for Echidna. Secondly, echidna can call into any contract with a known ABI by passing in the corresponding solidity source in the CLI. Use `multi-abi: true` in your config to turn this on. +On top of that, Echidna supports two modes of testing complex contracts. Firstly, one can [describe an initialization procedure with Truffle and Etheno](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/end-to-end-testing.md) and use that as the base state for Echidna. Secondly, Echidna can call into any contract with a known ABI by passing in the corresponding Solidity source in the CLI. Use `allContracts: true` in your config to turn this on. ### Crash course on Echidna diff --git a/flake.lock b/flake.lock index 82dd32a36..4165ccfaf 100644 --- a/flake.lock +++ b/flake.lock @@ -49,11 +49,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1676652184, - "narHash": "sha256-kV2mBquEaEuJyHwzO1dac5b/FFMwvqMrD7XmHS1Kwck=", + "lastModified": 1676994339, + "narHash": "sha256-Jnx9EhUtZVZjGe0weRc+OzYvNimq24Mi6JXUiaqW8wc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "223092f727b251eecf5e884fbaee4a660caec8fd", + "rev": "4c5c82e9cdb35494ae243ffe42f156269904d457", "type": "github" }, "original": { diff --git a/lib/Echidna/ABI.hs b/lib/Echidna/ABI.hs index d57f9b9ec..f22463d1f 100644 --- a/lib/Echidna/ABI.hs +++ b/lib/Echidna/ABI.hs @@ -1,10 +1,8 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE TemplateHaskell #-} module Echidna.ABI where -import Control.Lens import Control.Monad (join, liftM2, liftM3, foldM, replicateM) import Control.Monad.Random.Strict (MonadRandom, getRandom, getRandoms, getRandomR) import Control.Monad.Random.Strict qualified as R @@ -101,27 +99,27 @@ hashSig :: Text -> FunctionHash 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 (Set AbiValue) - -- ^ Constants to use, sorted by type - , _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 - , _rTypes :: Text -> Maybe AbiType - -- ^ Return types of any methods we scrape return values from - , _dictValues :: Set W256 - -- ^ A set of int/uint constants for better performance - } - -makeLenses 'GenDict +data GenDict = GenDict + { pSynthA :: Float + -- ^ Fraction of time to use dictionary vs. synthesize + , constants :: HashMap AbiType (Set AbiValue) + -- ^ Constants to use, sorted by type + , 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 + , rTypes :: Text -> Maybe AbiType + -- ^ Return types of any methods we scrape return values from + , dictValues :: Set W256 + -- ^ A set of int/uint constants for better performance + } 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 :: Set SolCall -> GenDict -> GenDict -gaddCalls c = wholeCalls <>~ hashMapBy (fmap $ fmap abiValueType) c +gaddCalls calls dict = + dict { wholeCalls = dict.wholeCalls <> hashMapBy (fmap $ fmap abiValueType) calls } defaultDict :: GenDict defaultDict = mkGenDict 0 Set.empty Set.empty 0 (const Nothing) @@ -307,7 +305,7 @@ genWithDict :: (Eq a, Hashable a, MonadRandom m) => 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 + 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 @@ -315,7 +313,7 @@ genWithDict genDict m g t = do -- | Synthesize a random 'AbiValue' given its 'AbiType'. Requires a dictionary. genAbiValueM :: MonadRandom m => GenDict -> AbiType -> m AbiValue -genAbiValueM genDict = genWithDict genDict 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) @@ -334,7 +332,7 @@ genAbiValueM genDict = genWithDict genDict genDict._constants $ \case genAbiCallM :: MonadRandom m => GenDict -> SolSignature -> m SolCall genAbiCallM genDict abi = do solCall <- genWithDict genDict - genDict._wholeCalls + genDict.wholeCalls (traverse $ traverse (genAbiValueM genDict)) abi mutateAbiCall solCall diff --git a/lib/Echidna/Campaign.hs b/lib/Echidna/Campaign.hs index 9c4de7ee3..762e4e60e 100644 --- a/lib/Echidna/Campaign.hs +++ b/lib/Echidna/Campaign.hs @@ -221,13 +221,16 @@ callseq ic v w ql = do -- Keep track of the number of calls to `callseq` ncallseqs += 1 -- Now we try to parse the return values as solidity constants, and add then to the 'GenDict' - types <- gets (._genDict._rTypes) + types <- gets (._genDict.rTypes) let results = parse (map (\(t, (vr, _)) -> (t, vr)) res) types -- union the return results with the new addresses additions = H.unionWith Set.union diffs results -- append to the constants dictionary - modifying (genDict . constants) . H.unionWith Set.union $ additions - modifying (genDict . dictValues) . Set.union $ mkDictValues $ Set.unions $ H.elems additions + let dict = camp._genDict + genDict .= dict + { constants = H.unionWith Set.union additions dict.constants + , dictValues = Set.union (mkDictValues $ Set.unions $ H.elems additions) dict.dictValues + } 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 @@ -258,8 +261,8 @@ campaign campaign u vm w ts d txs = do conf <- asks (.cfg.campaignConf) let c = fromMaybe mempty conf.knownCoverage - let effectiveSeed = fromMaybe d'._defSeed conf.seed - effectiveGenDict = d' { _defSeed = effectiveSeed } + let effectiveSeed = fromMaybe d'.defSeed conf.seed + effectiveGenDict = d' { defSeed = effectiveSeed } d' = fromMaybe defaultDict d execStateT (evalRandT runCampaign (mkStdGen effectiveSeed)) diff --git a/lib/Echidna/Config.hs b/lib/Echidna/Config.hs index 91bafd4fd..076a88cc1 100644 --- a/lib/Echidna/Config.hs +++ b/lib/Echidna/Config.hs @@ -16,6 +16,7 @@ import Data.Text (isPrefixOf) import Data.Yaml qualified as Y import EVM (VM(..)) +import EVM.Types (W256) import Echidna.Test import Echidna.Types.Campaign @@ -51,15 +52,21 @@ instance FromJSON EConfigWithUsage where let useKey k = modify' $ insert k x ..:? k = useKey k >> lift (x .:? k) x ..!= y = fromMaybe y <$> x - getWord s d = fromIntegral <$> v ..:? s ..!= (d :: Integer) + -- Parse as unbounded Integer and see if it fits into W256 + getWord256 k def = do + value :: Integer <- fromMaybe (fromIntegral (def :: W256)) <$> v ..:? k + if value > fromIntegral (maxBound :: W256) then + fail $ show k <> ": value does not fit in 256 bits" + else + pure $ fromIntegral value -- TxConf - xc = TxConf <$> getWord "propMaxGas" maxGasPerBlock - <*> getWord "testMaxGas" maxGasPerBlock - <*> getWord "maxGasprice" 0 - <*> getWord "maxTimeDelay" defaultTimeDelay - <*> getWord "maxBlockDelay" defaultBlockDelay - <*> getWord "maxValue" 100000000000000000000 -- 100 eth + xc = TxConf <$> v ..:? "propMaxGas" ..!= maxGasPerBlock + <*> v ..:? "testMaxGas" ..!= maxGasPerBlock + <*> getWord256 "maxGasprice" 0 + <*> getWord256 "maxTimeDelay" defaultTimeDelay + <*> getWord256 "maxBlockDelay" defaultBlockDelay + <*> getWord256 "maxValue" 100000000000000000000 -- 100 eth -- TestConf tc = do @@ -103,7 +110,7 @@ instance FromJSON EConfigWithUsage where <*> v ..:? "initialize" ..!= Nothing <*> v ..:? "deployContracts" ..!= [] <*> v ..:? "deployBytecodes" ..!= [] - <*> v ..:? "multi-abi" ..!= False + <*> v ..:? "allContracts" ..!= False <*> mode <*> v ..:? "testDestruction" ..!= False <*> v ..:? "allowFFI" ..!= False diff --git a/lib/Echidna/Fetch.hs b/lib/Echidna/Fetch.hs index 38f894c60..da6251374 100644 --- a/lib/Echidna/Fetch.hs +++ b/lib/Echidna/Fetch.hs @@ -33,7 +33,7 @@ deployBytecodes' di ((a, bc):cs) d vm = where zeros = pack $ replicate 320 0 -- This will initialize with zero a large number of possible constructor parameters loadRest = do - vm' <- execStateT (execTx $ createTx (bc `append` zeros) d a (fromInteger unlimitedGasPerBlock) (0, 0)) vm + vm' <- execStateT (execTx $ createTx (bc `append` zeros) d a unlimitedGasPerBlock (0, 0)) vm case vm'._result of (Just (VMSuccess _)) -> return vm' _ -> throwM $ DeploymentFailed a (Data.Text.unlines $ extractEvents True di vm') diff --git a/lib/Echidna/Output/JSON.hs b/lib/Echidna/Output/JSON.hs index 1e0bd5259..6103eea4d 100644 --- a/lib/Echidna/Output/JSON.hs +++ b/lib/Echidna/Output/JSON.hs @@ -97,7 +97,7 @@ encodeCampaign C.Campaign{..} = encode Campaign { _success = True , _error = Nothing , _tests = mapTest <$> _tests - , seed = _genDict._defSeed + , seed = _genDict.defSeed , coverage = mapKeys (("0x" ++) . (`showHex` "") . keccak') $ DF.toList <$>_coverage , gasInfo = toList _gasInfo } diff --git a/lib/Echidna/RPC.hs b/lib/Echidna/RPC.hs index 797c22284..c824cd776 100644 --- a/lib/Echidna/RPC.hs +++ b/lib/Echidna/RPC.hs @@ -160,6 +160,6 @@ execEthenoTxs et = do -- | For an etheno txn, set up VM to execute txn setupEthenoTx :: MonadState VM m => Etheno -> m () setupEthenoTx (AccountCreated f) = initAddress f -- TODO: improve etheno to include initial balance -setupEthenoTx (ContractCreated f c _ _ d v) = setupTx $ createTxWithValue d f c (fromInteger unlimitedGasPerBlock) v (1, 1) -setupEthenoTx (FunctionCall f t _ _ d v) = setupTx $ Tx (SolCalldata d) f t (fromInteger unlimitedGasPerBlock) 0 v (1, 1) +setupEthenoTx (ContractCreated f c _ _ d v) = setupTx $ createTxWithValue d f c unlimitedGasPerBlock v (1, 1) +setupEthenoTx (FunctionCall f t _ _ d v) = setupTx $ Tx (SolCalldata d) f t unlimitedGasPerBlock 0 v (1, 1) setupEthenoTx (BlockMined n t) = setupTx $ Tx NoCall 0 0 0 0 0 (fromInteger t, fromInteger n) diff --git a/lib/Echidna/Solidity.hs b/lib/Echidna/Solidity.hs index f0fc8aaef..deaa4e9be 100644 --- a/lib/Echidna/Solidity.hs +++ b/lib/Echidna/Solidity.hs @@ -184,7 +184,7 @@ loadSpecified solConf name cs = do -- Set up initial VM, either with chosen contract or Etheno initialization file -- need to use snd to add to ABI dict - let vm = initialVM ffi & block . gaslimit .~ fromInteger unlimitedGasPerBlock + let vm = initialVM ffi & block . gaslimit .~ unlimitedGasPerBlock & block . maxCodeSize .~ fromInteger mcs blank' <- maybe (pure vm) (loadEthenoBatch ffi) fp let blank = populateAddresses (Set.insert d ads) bala blank' @@ -218,12 +218,12 @@ loadSpecified solConf name cs = do vm2 <- deployBytecodes di dpb d vm1 -- main contract deployment - let deployment = execTx $ createTxWithValue bc d ca (fromInteger unlimitedGasPerBlock) (fromInteger balc) (0, 0) + let deployment = execTx $ createTxWithValue bc d ca unlimitedGasPerBlock (fromInteger balc) (0, 0) vm3 <- execStateT deployment vm2 when (isNothing $ currentContract vm3) (throwM $ DeploymentFailed ca $ T.unlines $ extractEvents True di vm3) -- Run - let transaction = execTx $ uncurry basicTx setUpFunction d ca (fromInteger unlimitedGasPerBlock) (0, 0) + let transaction = execTx $ uncurry basicTx setUpFunction d ca unlimitedGasPerBlock (0, 0) vm4 <- if isDapptestMode tm && setUpFunction `elem` abi then execStateT transaction vm3 else return vm3 case vm4._result of diff --git a/lib/Echidna/Transaction.hs b/lib/Echidna/Transaction.hs index 82636bccc..b2cb33e28 100644 --- a/lib/Echidna/Transaction.hs +++ b/lib/Echidna/Transaction.hs @@ -51,7 +51,7 @@ genTxM memo m = do World ss hmm lmm ps _ <- asks fst genDict <- gets (._genDict) mm <- getSignatures hmm lmm - let ns = genDict._dictValues + let ns = genDict.dictValues s' <- rElem' ss r' <- rElem' $ Set.fromList (mapMaybe (toContractA mm) (toList m)) c' <- genInteractionsM genDict (snd r') diff --git a/lib/Echidna/Types/Solidity.hs b/lib/Echidna/Types/Solidity.hs index d14de6d60..3303e1252 100644 --- a/lib/Echidna/Types/Solidity.hs +++ b/lib/Echidna/Types/Solidity.hs @@ -72,7 +72,7 @@ data SolConf = SolConf , initialize :: Maybe FilePath -- ^ Initialize world with Etheno txns , deployContracts :: [(Addr, String)] -- ^ List of contracts to deploy in specific addresses , deployBytecodes :: [(Addr, Text)] -- ^ List of contracts to deploy in specific addresses - , multiAbi :: Bool -- ^ Whether or not to use the multi-abi mode + , allContracts :: Bool -- ^ Whether or not to fuzz all contracts , testMode :: String -- ^ Testing mode , testDestruction :: Bool -- ^ Whether or not to add a property to detect contract destruction , allowFFI :: Bool -- ^ Whether or not to allow FFI hevm cheatcode diff --git a/lib/Echidna/Types/Tx.hs b/lib/Echidna/Types/Tx.hs index 0973a250b..ef3ee98dc 100644 --- a/lib/Echidna/Types/Tx.hs +++ b/lib/Echidna/Types/Tx.hs @@ -31,16 +31,16 @@ data TxCall = SolCreate ByteString deriving (Show, Ord, Eq) $(deriveJSON defaultOptions ''TxCall) -maxGasPerBlock :: Integer +maxGasPerBlock :: Word64 maxGasPerBlock = 12500000 -- https://cointelegraph.com/news/ethereum-miners-vote-to-increase-gas-limit-causing-community-debate -unlimitedGasPerBlock :: Integer +unlimitedGasPerBlock :: Word64 unlimitedGasPerBlock = 0xffffffff -defaultTimeDelay :: Integer +defaultTimeDelay :: W256 defaultTimeDelay = 604800 -defaultBlockDelay :: Integer +defaultBlockDelay :: W256 defaultBlockDelay = 60480 initialTimestamp :: W256 @@ -207,5 +207,5 @@ getResult (VMFailure (FFI _)) = ErrorFFI getResult (VMFailure NonceOverflow) = ErrorNonceOverflow makeSingleTx :: Addr -> Addr -> W256 -> TxCall -> [Tx] -makeSingleTx a d v (SolCall c) = [Tx (SolCall c) a d (fromInteger maxGasPerBlock) 0 v (0, 0)] +makeSingleTx a d v (SolCall c) = [Tx (SolCall c) a d maxGasPerBlock 0 v (0, 0)] makeSingleTx _ _ _ _ = error "invalid usage of makeSingleTx" diff --git a/lib/Echidna/UI.hs b/lib/Echidna/UI.hs index 61c2e0c3e..426ce8a15 100644 --- a/lib/Echidna/UI.hs +++ b/lib/Echidna/UI.hs @@ -19,7 +19,7 @@ import Control.Monad (when) import Control.Monad.State.Strict (get) #endif -import Control.Monad.Catch (MonadCatch(..)) +import Control.Monad.Catch (MonadCatch(..), catchAll) import Control.Monad.IO.Class (MonadIO(..)) import Control.Monad.Reader (MonadReader, runReader, asks) import Control.Monad.Random.Strict (MonadRandom) @@ -41,7 +41,10 @@ import Echidna.Types.World (World) import Echidna.UI.Report import Echidna.Types.Config -data CampaignEvent = CampaignUpdated Campaign | CampaignTimedout Campaign +data CampaignEvent = + CampaignUpdated Campaign + | CampaignTimedout Campaign + | CampaignCrashed String -- | Set up and run an Echidna 'Campaign' and display interactive UI or -- print non-interactive output in desired format at the end @@ -78,9 +81,12 @@ ui vm world ts d txs = do ticker <- liftIO $ forkIO $ -- run UI update every 100ms forever $ threadDelay 100000 >> updateUI CampaignUpdated _ <- forkFinally -- run worker - (void $ runCampaign >>= \case - Nothing -> liftIO $ updateUI CampaignTimedout - Just _ -> liftIO $ updateUI CampaignUpdated + (void $ do + catchAll + (runCampaign >>= \case + Nothing -> liftIO $ updateUI CampaignTimedout + Just _ -> liftIO $ updateUI CampaignUpdated) + (liftIO . writeBChan bc . CampaignCrashed . show) ) (const $ liftIO $ killThread ticker) let buildVty = do @@ -132,6 +138,9 @@ monitor = do onEvent (AppEvent (CampaignUpdated c')) = put (c', Running) onEvent (AppEvent (CampaignTimedout c')) = put (c', Timedout) + onEvent (AppEvent (CampaignCrashed e)) = do + (c,_) <- get + put (c, Crashed e) onEvent (VtyEvent (EvKey KEsc _)) = halt onEvent (VtyEvent (EvKey (KChar 'c') l)) | MCtrl `elem` l = halt onEvent (MouseDown (SBClick el n) _ _ _) = diff --git a/lib/Echidna/UI/Report.hs b/lib/Echidna/UI/Report.hs index c45ab5332..e25265418 100644 --- a/lib/Echidna/UI/Report.hs +++ b/lib/Echidna/UI/Report.hs @@ -124,7 +124,7 @@ ppCampaign c = do gasInfoPrinted <- ppGasInfo c let coveragePrinted = maybe "" ("\n" ++) . ppCoverage $ c._coverage corpusPrinted = maybe "" ("\n" ++) . ppCorpus $ c._corpus - seedPrinted = "\nSeed: " ++ show c._genDict._defSeed + seedPrinted = "\nSeed: " ++ show c._genDict.defSeed pure $ testsPrinted ++ gasInfoPrinted diff --git a/lib/Echidna/UI/Widgets.hs b/lib/Echidna/UI/Widgets.hs index ac03d65a0..3f8b9c798 100644 --- a/lib/Echidna/UI/Widgets.hs +++ b/lib/Echidna/UI/Widgets.hs @@ -26,7 +26,7 @@ import Echidna.Types.Tx (Tx(..), TxResult(..)) import Echidna.UI.Report import Echidna.Types.Config -data UIState = Uninitialized | Running | Timedout +data UIState = Uninitialized | Running | Timedout | Crashed String attrs :: A.AttrMap attrs = A.attrMap (V.white `on` V.black) @@ -49,31 +49,42 @@ campaignStatus :: MonadReader EConfig m campaignStatus (c@Campaign{_tests, _coverage, _ncallseqs}, uiState) = do done <- isDone c case (uiState, done) of - (Uninitialized, _) -> pure $ mainbox (padLeft (Pad 1) $ str "Starting up, please wait...") emptyWidget - (Timedout, _) -> mainbox <$> testsWidget _tests <*> pure (str "Timed out, C-c or esc to exit") - (_, True) -> mainbox <$> testsWidget _tests <*> pure (str "Campaign complete, C-c or esc to exit") - _ -> mainbox <$> testsWidget _tests <*> pure emptyWidget + (Uninitialized, _) -> + pure $ mainbox (padLeft (Pad 1) $ str "Starting up, please wait...") emptyWidget + (Crashed e, _) -> + pure $ mainbox (padLeft (Pad 1) $ + withAttr (attrName "failure") $ strBreak $ formatCrashReport e) emptyWidget + (Timedout, _) -> + mainboxTests (str "Timed out, C-c or esc to exit") + (_, True) -> + mainboxTests (str "Campaign complete, C-c or esc to exit") + _ -> + mainboxTests emptyWidget where + mainboxTests underneath = do + t <- testsWidget _tests + pure $ mainbox (summaryWidget c <=> hBorderWithLabel (str "Tests") <=> t) underneath + mainbox :: Widget Name -> Widget Name -> Widget Name mainbox inner underneath = padTop (Pad 1) $ hCenter $ hLimit 120 $ wrapInner inner <=> hCenter underneath - wrapInner inner = - borderWithLabel (withAttr (attrName "bold") $ str title) $ - summaryWidget c - <=> - hBorderWithLabel (str "Tests") - <=> - inner + wrapInner = borderWithLabel (withAttr (attrName "bold") $ str title) title = "Echidna " ++ showVersion Paths_echidna.version +formatCrashReport :: String -> String +formatCrashReport e = + "Echidna crashed with an error:\n\n" <> + e <> + "\n\nPlease report it to https://github.com/crytic/echidna/issues" + summaryWidget :: Campaign -> Widget Name summaryWidget c = padLeft (Pad 1) ( str ("Tests found: " ++ show (length c._tests)) <=> - str ("Seed: " ++ show c._genDict._defSeed) + str ("Seed: " ++ show c._genDict.defSeed) <=> maybe emptyWidget str (ppCoverage c._coverage) <=> @@ -132,7 +143,7 @@ eventWidget :: Events -> Widget n eventWidget es = if null es then str "" else str "Event sequence" <+> str ":" - <=> strWrapWith wrapSettings (T.unpack $ T.intercalate "\n" es) + <=> strBreak (T.unpack $ T.intercalate "\n" es) failWidget :: MonadReader EConfig m => Maybe (Int, Int) -> [Tx] -> Events -> TestValue -> TxResult -> m (Widget Name, Widget Name) @@ -177,7 +188,7 @@ seqWidget xs = do let ordinals = str . printf "%d." <$> [1 :: Int ..] pure $ foldl (<=>) emptyWidget $ - zipWith (<+>) ordinals (withAttr (attrName "tx") . strWrapWith wrapSettings <$> ppTxs) + zipWith (<+>) ordinals (withAttr (attrName "tx") . strBreak <$> ppTxs) failureBadge :: Widget Name failureBadge = withAttr (attrName "failure") $ str "FAILED!" @@ -185,7 +196,7 @@ failureBadge = withAttr (attrName "failure") $ str "FAILED!" maximumBadge :: Widget Name maximumBadge = withAttr (attrName "maximum") $ str "OPTIMIZED!" -wrapSettings :: WrapSettings -wrapSettings = defaultWrapSettings { breakLongWords = True } +strBreak :: String -> Widget n +strBreak = strWrapWith $ defaultWrapSettings { breakLongWords = True } -#endif \ No newline at end of file +#endif diff --git a/package.yaml b/package.yaml index b4796e731..9ced08e33 100644 --- a/package.yaml +++ b/package.yaml @@ -61,11 +61,6 @@ library: source-dirs: lib/ when: - - condition: os(darwin) - extra-libraries: c++ - ld-options: -Wl,-keep_dwarf_unwind - - condition: os(windows) - extra-libraries: stdc++ - condition: "!os(windows)" cpp-options: -DINTERACTIVE_UI dependencies: @@ -89,6 +84,11 @@ executables: ghc-options: - -O2 ld-options: -pthread + - condition: os(darwin) + extra-libraries: c++ + ld-options: -Wl,-keep_dwarf_unwind + - condition: os(windows) + extra-libraries: stdc++ tests: echidna-testsuite: @@ -110,6 +110,11 @@ tests: ghc-options: - -O2 ld-options: -pthread + - condition: os(darwin) + extra-libraries: c++ + ld-options: -Wl,-keep_dwarf_unwind + - condition: os(windows) + extra-libraries: stdc++ flags: static: diff --git a/src/Main.hs b/src/Main.hs index 19cbffb66..ca1f6e3a6 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -80,7 +80,7 @@ data Options = Options , cliOutputFormat :: Maybe OutputFormat , cliCorpusDir :: Maybe FilePath , cliTestMode :: Maybe TestMode - , cliMultiAbi :: Bool + , cliAllContracts :: Bool , cliTestLimit :: Maybe Int , cliShrinkLimit :: Maybe Int , cliSeqLen :: Maybe Int @@ -115,8 +115,8 @@ options = Options <> help "Directory to save and load corpus and coverage data.") <*> optional (option str $ long "test-mode" <> help "Test mode to use. Either 'property', 'assertion', 'dapptest', 'optimization', 'overflow' or 'exploration'" ) - <*> switch (long "multi-abi" - <> help "Use multi-abi mode of testing.") + <*> switch (long "all-contracts" + <> help "Generate calls to all deployed contracts.") <*> optional (option auto $ long "test-limit" <> metavar "INTEGER" <> help ("Number of sequences of transactions to generate during testing. Default is " ++ show defaultTestLimit)) @@ -180,5 +180,5 @@ overrideConfig config Options{..} = , deployer = fromMaybe solConf.deployer cliDeployer , contractAddr = fromMaybe solConf.contractAddr cliContractAddr , testMode = maybe solConf.testMode validateTestMode cliTestMode - , multiAbi = cliMultiAbi || solConf.multiAbi + , allContracts = cliAllContracts || solConf.allContracts } diff --git a/src/test/Tests/Config.hs b/src/test/Tests/Config.hs index f1aad7f66..74bcc53e5 100644 --- a/src/test/Tests/Config.hs +++ b/src/test/Tests/Config.hs @@ -1,14 +1,16 @@ module Tests.Config (configTests) where import Test.Tasty (TestTree, testGroup) -import Test.Tasty.HUnit (testCase, assertBool, (@?=)) +import Test.Tasty.HUnit (testCase, assertBool, (@?=), assertFailure) import Control.Lens (sans) import Control.Monad (void) import Data.Function ((&)) +import Data.Yaml qualified as Y import Echidna.Types.Config (EConfigWithUsage(..), EConfig(..)) import Echidna.Types.Campaign (CampaignConf(..)) +import Echidna.Types.Tx (TxConf(..)) import Echidna.Config (defaultConfig, parseConfig) configTests :: TestTree @@ -24,6 +26,16 @@ configTests = testGroup "Configuration tests" $ assertBool ("unused options: " ++ show bad) $ null bad let unset' = unset & sans "seed" assertBool ("unset options: " ++ show unset') $ null unset' + , testCase "W256 decoding" $ do + let maxW256 = "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + overW256 = "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0" + case Y.decodeEither' ("maxGasprice: " <> maxW256) of + Right (c :: EConfigWithUsage) | c.econfig.txConf.maxGasprice == maxBound -> pure () + Right _ -> assertFailure "wrong value decoded" + Left e -> assertFailure $ "unexpected decoding error: " <> show e + case Y.decodeEither' ("maxGasprice: " <> overW256) of + Right (_ :: EConfigWithUsage) -> assertFailure "should not decode" + Left _ -> pure () ] where files = ["basic/config.yaml", "basic/default.yaml"] assertCoverage config value = config.campaignConf.knownCoverage @?= value diff --git a/src/test/Tests/Integration.hs b/src/test/Tests/Integration.hs index 4da9a6dc9..bb0303609 100644 --- a/src/test/Tests/Integration.hs +++ b/src/test/Tests/Integration.hs @@ -57,7 +57,7 @@ integrationTests = testGroup "Solidity Integration Testing" ] , testContract "basic/library.sol" (Just "basic/library.yaml") [ ("echidna_library_call failed", solved "echidna_library_call") - , ("echidna_valid_timestamp failed", passed "echidna_valid_timestamp") + , ("echidna_valid_timestamp failed", passed "echidna_valid_timestamp") ] , testContractV "basic/fallback.sol" (Just (< solcV (0,6,0))) Nothing [ ("echidna_fallback failed", solved "echidna_fallback") ] @@ -74,7 +74,7 @@ integrationTests = testGroup "Solidity Integration Testing" [ ("echidna_construct passed", solved "echidna_construct") ] , testContract "basic/gasprice.sol" (Just "basic/gasprice.yaml") [ ("echidna_state passed", solved "echidna_state") ] - , testContract' "basic/multi-abi.sol" (Just "B") Nothing (Just "basic/multi-abi.yaml") True + , testContract' "basic/allContracts.sol" (Just "B") Nothing (Just "basic/allContracts.yaml") True [ ("echidna_test passed", solved "echidna_test") ] , testContract "basic/array-mutation.sol" Nothing [ ("echidna_mutated passed", solved "echidna_mutated") ] @@ -94,7 +94,7 @@ integrationTests = testGroup "Solidity Integration Testing" , checkConstructorConditions "basic/codesize.sol" "invalid codesize" , testContractV "basic/eip-170.sol" (Just (>= solcV (0,5,0))) (Just "basic/eip-170.yaml") - [ ("echidna_test passed", passed "echidna_test") ] + [ ("echidna_test passed", passed "echidna_test") ] , testContract' "basic/deploy.sol" (Just "Test") Nothing (Just "basic/deployContract.yaml") True [ ("test passed", solved "test") ] , testContract' "basic/deploy.sol" (Just "Test") Nothing (Just "basic/deployBytecode.yaml") True diff --git a/tests/solidity/assert/multi.yaml b/tests/solidity/assert/multi.yaml index 643cbf3fd..82643f037 100644 --- a/tests/solidity/assert/multi.yaml +++ b/tests/solidity/assert/multi.yaml @@ -1,2 +1,2 @@ testMode: assertion -multi-abi: true +allContracts: true diff --git a/tests/solidity/basic/multi-abi.sol b/tests/solidity/basic/allContracts.sol similarity index 100% rename from tests/solidity/basic/multi-abi.sol rename to tests/solidity/basic/allContracts.sol diff --git a/tests/solidity/basic/allContracts.yaml b/tests/solidity/basic/allContracts.yaml new file mode 100644 index 000000000..8f7ae58ba --- /dev/null +++ b/tests/solidity/basic/allContracts.yaml @@ -0,0 +1 @@ +allContracts: true diff --git a/tests/solidity/basic/default.yaml b/tests/solidity/basic/default.yaml index 732f83f48..5d31933de 100644 --- a/tests/solidity/basic/default.yaml +++ b/tests/solidity/basic/default.yaml @@ -54,8 +54,8 @@ initialize: null deployContracts: [] #initialize the blockchain with some bytecode in some addresses deployBytecodes: [] -#whether ot not to use the multi-abi mode of testing -multi-abi: false +#whether ot not to fuzz all contracts +allContracts: false #timeout controls test timeout settings timeout: null #seed not defined by default, is the random seed diff --git a/tests/solidity/basic/multi-abi.yaml b/tests/solidity/basic/multi-abi.yaml deleted file mode 100644 index 8b3fc2674..000000000 --- a/tests/solidity/basic/multi-abi.yaml +++ /dev/null @@ -1 +0,0 @@ -multi-abi: true