Skip to content

Commit c71b493

Browse files
authored
Merge pull request #301 from crytic/config-warn
Warn on unused config keys
2 parents 25a3009 + 654811f commit c71b493

File tree

4 files changed

+119
-60
lines changed

4 files changed

+119
-60
lines changed

examples/solidity/basic/default.yaml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ prefix: "echidna_"
77
propMaxGas: 8000030
88
#testMaxGas is a gas limit; does not cause failure, but terminates sequence
99
testMaxGas: 0xffffffff
10+
#maxGasprice is the maximum gas price
11+
maxGasprice: 100000000000
1012
#testLimit is the number of test sequences to run
1113
testLimit: 50000
1214
#stopOnFail makes echidna terminate as soon as any property fails and has been shrunk
@@ -15,6 +17,8 @@ stopOnFail: false
1517
seqLen: 100
1618
#shrinkLimit determines how much effort is spent shrinking failing sequences
1719
shrinkLimit: 5000
20+
#coverage controls coverage guided testing
21+
coverage: false
1822
#format can be "text" or "json" for different output (human or machine readable)
1923
format: "text"
2024
#contractAddr is the address of the contract itself
@@ -27,15 +31,25 @@ sender: ["0x10000", "0x20000", "0x00a329c0648769a73afac7f9381e08fb43dbea70"]
2731
balanceAddr: 0xffffffff
2832
#balanceContract overrides balanceAddr for the contract address
2933
balanceContract: 0
30-
#solcArgs allows special Args to solc
34+
#solcArgs allows special args to solc
3135
solcArgs: ""
3236
#solcLibs is solc libraries
3337
solcLibs: []
38+
#cryticArgs allows special args to crytic
39+
cryticArgs: []
3440
#quiet produces (much) less verbose output
3541
quiet: false
42+
#checkAsserts checks assertions
43+
checkAsserts: false
3644
#dashboard determines if output is just text or an AFL-like display
3745
dashboard: true
46+
#timeout controls test timeout settings
47+
timeout: null
3848
#seed not defined by default, is the random seed
49+
#seed: 0
50+
#dictFreq controls how often to use echidna's internal dictionary vs random
51+
#values
52+
dictFreq: 0.40
3953
maxTimeDelay: 604800
4054
#maximum time between generated txs; default is one week
4155
maxBlockDelay: 60480

lib/Echidna/Config.hs

Lines changed: 90 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{-# LANGUAGE FlexibleContexts #-}
12
{-# LANGUAGE FlexibleInstances #-}
23
{-# LANGUAGE LambdaCase #-}
34
{-# LANGUAGE MultiParamTypeClasses #-}
@@ -12,12 +13,17 @@ import Control.Monad (liftM2, liftM5)
1213
import Control.Monad.Catch (MonadThrow)
1314
import Control.Monad.IO.Class (MonadIO(..))
1415
import Control.Monad.Reader (Reader, ReaderT(..), runReader)
16+
import Control.Monad.State (StateT(..), runStateT)
17+
import Control.Monad.Trans (lift)
1518
import Data.ByteString.Lazy.Char8 (unpack)
16-
import Data.Has (Has(..))
1719
import Data.Aeson
1820
import Data.Aeson.Lens
1921
import Data.Functor ((<&>))
20-
import Data.Text (isPrefixOf)
22+
import Data.Has (Has(..))
23+
import Data.HashMap.Strict (keys)
24+
import Data.HashSet (HashSet, fromList, insert, difference)
25+
import Data.Maybe (fromMaybe)
26+
import Data.Text (Text, isPrefixOf)
2127
import EVM (result)
2228
import EVM.Concrete (Word(..), Whiff(..))
2329

@@ -42,6 +48,15 @@ data EConfig = EConfig { _cConf :: CampaignConf
4248
}
4349
makeLenses ''EConfig
4450

51+
data EConfigWithUsage = EConfigWithUsage { _econfig :: EConfig
52+
, _badkeys :: HashSet Text
53+
, _unsetkeys :: HashSet Text
54+
}
55+
makeLenses ''EConfigWithUsage
56+
57+
instance Has EConfig EConfigWithUsage where
58+
hasLens = econfig
59+
4560
instance Has CampaignConf EConfig where
4661
hasLens = cConf
4762

@@ -61,63 +76,85 @@ instance Has UIConf EConfig where
6176
hasLens = uConf
6277

6378
instance FromJSON EConfig where
64-
parseJSON (Object v) =
65-
let tc = do psender <- v .:? "psender" .!= 0x00a329c0648769a73afac7f9381e08fb43dbea70
66-
fprefix <- v .:? "prefix" .!= "echidna_"
67-
let goal fname = if (fprefix <> "revert_") `isPrefixOf` fname then ResRevert else ResTrue
68-
return $ TestConf (\fname -> (== goal fname) . maybe ResOther classifyRes . view result)
69-
(const psender)
70-
getWord s d = C Dull . fromIntegral <$> v .:? s .!= (d :: Integer)
71-
xc = liftM5 TxConf (getWord "propMaxGas" 8000030) (getWord "testMaxGas" 0xffffffff)
72-
(getWord "maxGasprice" 100000000000)
73-
(getWord "maxTimeDelay" 604800) (getWord "maxBlockDelay" 60480)
74-
cov = v .:? "coverage" <&> \case Just True -> Just mempty
75-
_ -> Nothing
76-
cc = CampaignConf <$> v .:? "testLimit" .!= 50000
77-
<*> v .:? "stopOnFail" .!= False
78-
<*> v .:? "seqLen" .!= 100
79-
<*> v .:? "shrinkLimit" .!= 5000
80-
<*> cov
81-
<*> v .:? "seed"
82-
<*> v .:? "dictFreq" .!= 0.40
83-
84-
names :: Names
85-
names Sender = (" from: " ++) . show
86-
names _ = const ""
87-
ppc :: Y.Parser (Campaign -> Int -> String)
88-
ppc = liftM2 (\cf xf c g -> runReader (ppCampaign c) (cf, xf, names) ++ "\nSeed: " ++ show g) cc xc
89-
style :: Y.Parser (Campaign -> Int -> String)
90-
style = v .:? "format" .!= ("text" :: String) >>=
91-
\case "text" -> ppc
92-
"json" -> pure . flip $ \g ->
93-
unpack . encode . set (_Object . at "seed") (Just . toJSON $ g) . toJSON;
94-
"none" -> pure $ \_ _ -> ""
95-
_ -> pure $ \_ _ -> M.fail
96-
"unrecognized ui type (should be text, json, or none)" in
97-
EConfig <$> cc
98-
<*> pure names
99-
<*> (SolConf <$> v .:? "contractAddr" .!= 0x00a329c0648769a73afac7f9381e08fb43dbea72
100-
<*> v .:? "deployer" .!= 0x00a329c0648769a73afac7f9381e08fb43dbea70
101-
<*> v .:? "sender" .!= NE.fromList [0x10000, 0x20000, 0x00a329c0648769a73afac7f9381e08fb43dbea70]
102-
<*> v .:? "balanceAddr" .!= 0xffffffff
103-
<*> v .:? "balanceContract".!= 0
104-
<*> v .:? "prefix" .!= "echidna_"
105-
<*> v .:? "cryticArgs" .!= []
106-
<*> v .:? "solcArgs" .!= ""
107-
<*> v .:? "solcLibs" .!= []
108-
<*> v .:? "quiet" .!= False
109-
<*> v .:? "checkAsserts" .!= False)
110-
<*> tc
111-
<*> xc
112-
<*> (UIConf <$> v .:? "dashboard" .!= True <*> v .:? "timeout" <*> style)
113-
parseJSON _ = parseJSON (Object mempty)
79+
-- retrieve the config from the key usage annotated parse
80+
parseJSON = fmap _econfig . parseJSON
81+
82+
instance FromJSON EConfigWithUsage where
83+
-- this runs the parser in a StateT monad which keeps track of the keys
84+
-- utilized by the config parser
85+
-- we can then compare the set difference between the keys found in the config
86+
-- file and the keys used by the parser to comopute which keys were set in the
87+
-- config and not used and which keys were unset in the config and defaulted
88+
parseJSON o = do
89+
let v' = case o of
90+
Object v -> v
91+
_ -> mempty
92+
(c, ks) <- runStateT (parser v') $ fromList []
93+
let found = fromList (keys v')
94+
return $ EConfigWithUsage c (found `difference` ks) (ks `difference` found)
95+
-- this parser runs in StateT and comes equipped with the following
96+
-- equivalent unary operators:
97+
-- x .:? k (Parser) <==> x ..:? k (StateT)
98+
-- x .!= v (Parser) <==> x ..!= v (StateT)
99+
-- tl;dr use an extra initial . to lift into the StateT parser
100+
where parser v =
101+
let useKey k = hasLens %= insert k
102+
x ..:? k = useKey k >> lift (x .:? k)
103+
x ..!= y = fromMaybe y <$> x
104+
tc = do psender <- v ..:? "psender" ..!= 0x00a329c0648769a73afac7f9381e08fb43dbea70
105+
fprefix <- v ..:? "prefix" ..!= "echidna_"
106+
let goal fname = if (fprefix <> "revert_") `isPrefixOf` fname then ResRevert else ResTrue
107+
return $ TestConf (\fname -> (== goal fname) . maybe ResOther classifyRes . view result)
108+
(const psender)
109+
getWord s d = C Dull . fromIntegral <$> v ..:? s ..!= (d :: Integer)
110+
xc = liftM5 TxConf (getWord "propMaxGas" 8000030) (getWord "testMaxGas" 0xffffffff)
111+
(getWord "maxGasprice" 100000000000)
112+
(getWord "maxTimeDelay" 604800) (getWord "maxBlockDelay" 60480)
113+
cov = v ..:? "coverage" <&> \case Just True -> Just mempty
114+
_ -> Nothing
115+
cc = CampaignConf <$> v ..:? "testLimit" ..!= 50000
116+
<*> v ..:? "stopOnFail" ..!= False
117+
<*> v ..:? "seqLen" ..!= 100
118+
<*> v ..:? "shrinkLimit" ..!= 5000
119+
<*> cov
120+
<*> v ..:? "seed"
121+
<*> v ..:? "dictFreq" ..!= 0.40
122+
names :: Names
123+
names Sender = (" from: " ++) . show
124+
names _ = const ""
125+
--ppc :: Has (HashSet Text) s => StateT s Y.Parser (Campaign -> Int -> String)
126+
ppc = liftM2 (\cf xf c g -> runReader (ppCampaign c) (cf, xf, names) ++ "\nSeed: " ++ show g) cc xc
127+
--style :: Has (HashSet Text) s => StateT s Y.Parser (Campaign -> Int -> String)
128+
style = v ..:? "format" ..!= ("text" :: String) >>=
129+
\case "text" -> ppc
130+
"json" -> pure . flip $ \g ->
131+
unpack . encode . set (_Object . at "seed") (Just . toJSON $ g) . toJSON
132+
"none" -> pure $ \_ _ -> ""
133+
_ -> pure $ \_ _ -> M.fail
134+
"unrecognized ui type (should be text, json, or none)" in
135+
EConfig <$> cc
136+
<*> pure names
137+
<*> (SolConf <$> v ..:? "contractAddr" ..!= 0x00a329c0648769a73afac7f9381e08fb43dbea72
138+
<*> v ..:? "deployer" ..!= 0x00a329c0648769a73afac7f9381e08fb43dbea70
139+
<*> v ..:? "sender" ..!= (0x10000 NE.:| [0x20000, 0x00a329c0648769a73afac7f9381e08fb43dbea70])
140+
<*> v ..:? "balanceAddr" ..!= 0xffffffff
141+
<*> v ..:? "balanceContract" ..!= 0
142+
<*> v ..:? "prefix" ..!= "echidna_"
143+
<*> v ..:? "cryticArgs" ..!= []
144+
<*> v ..:? "solcArgs" ..!= ""
145+
<*> v ..:? "solcLibs" ..!= []
146+
<*> v ..:? "quiet" ..!= False
147+
<*> v ..:? "checkAsserts" ..!= False)
148+
<*> tc
149+
<*> xc
150+
<*> (UIConf <$> v ..:? "dashboard" ..!= True <*> v ..:? "timeout" <*> style)
114151

115152
-- | The default config used by Echidna (see the 'FromJSON' instance for values used).
116153
defaultConfig :: EConfig
117154
defaultConfig = either (error "Config parser got messed up :(") id $ Y.decodeEither' ""
118155

119156
-- | Try to parse an Echidna config file, throw an error if we can't.
120-
parseConfig :: (MonadThrow m, MonadIO m) => FilePath -> m EConfig
157+
parseConfig :: (MonadThrow m, MonadIO m) => FilePath -> m EConfigWithUsage
121158
parseConfig f = liftIO (BS.readFile f) >>= Y.decodeThrow
122159

123160
-- | Run some action with the default configuration, useful in the REPL.

src/Main.hs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
module Main where
22

3-
import Control.Lens (view)
3+
import Control.Lens (view, (^.))
4+
import Control.Monad (unless)
45
import Control.Monad.Reader (runReaderT)
56
import Control.Monad.Random (getRandom)
6-
import Data.Text (pack)
7+
import Data.Text (pack, unpack)
78
import Data.Version (showVersion)
89
import Options.Applicative
910
import Paths_echidna (version)
1011
import System.Exit (exitWith, exitSuccess, ExitCode(..))
12+
import System.IO (hPutStrLn, stderr)
1113

1214
import Echidna.ABI
1315
import Echidna.Config
@@ -44,7 +46,8 @@ opts = info (helper <*> versionOption <*> options) $ fullDesc
4446
main :: IO ()
4547
main = do Options f c conf <- execParser opts
4648
g <- getRandom
47-
cfg <- maybe (pure defaultConfig) parseConfig conf
49+
EConfigWithUsage cfg ks _ <- maybe (pure (EConfigWithUsage defaultConfig mempty mempty)) parseConfig conf
50+
unless (cfg ^. sConf . quiet) $ mapM_ (hPutStrLn stderr . ("Warning: unused option: " ++) . unpack) ks
4851
cpg <- flip runReaderT cfg $ do
4952
cs <- contracts f
5053
ads <- addresses

src/test/Spec.hs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Test.Tasty.HUnit
77

88
import Echidna.ABI (SolCall, mkGenDict)
99
import Echidna.Campaign (Campaign(..), CampaignConf(..), TestState(..), campaign, tests)
10-
import Echidna.Config (EConfig, defaultConfig, parseConfig, sConf, cConf)
10+
import Echidna.Config (EConfig, EConfigWithUsage(..), _econfig, defaultConfig, parseConfig, sConf, cConf)
1111
import Echidna.Solidity
1212
import Echidna.Transaction (Tx, call)
1313

@@ -38,10 +38,15 @@ configTests :: TestTree
3838
configTests = testGroup "Configuration tests" $
3939
[ testCase file $ void $ parseConfig file | file <- files ] ++
4040
[ testCase "parse \"coverage: true\"" $ do
41-
config <- parseConfig "coverage/test.yaml"
41+
config <- _econfig <$> parseConfig "coverage/test.yaml"
4242
assertCoverage config $ Just mempty
4343
, testCase "coverage disabled by default" $
4444
assertCoverage defaultConfig Nothing
45+
, testCase "defaults.yaml" $ do
46+
EConfigWithUsage _ bad unset <- parseConfig "basic/default.yaml"
47+
assertBool ("unused options: " ++ show bad) $ null bad
48+
let unset' = unset & sans "seed"
49+
assertBool ("unset options: " ++ show unset') $ null unset'
4550
]
4651
where files = ["basic/config.yaml", "basic/default.yaml"]
4752
assertCoverage config value = do
@@ -189,7 +194,7 @@ integrationTests = testGroup "Solidity Integration Testing"
189194

190195
testContract :: FilePath -> Maybe FilePath -> [(String, Campaign -> Bool)] -> TestTree
191196
testContract fp cfg as = testCase fp $ do
192-
c <- set (sConf . quiet) True <$> maybe (pure defaultConfig) parseConfig cfg
197+
c <- set (sConf . quiet) True <$> maybe (pure defaultConfig) (fmap _econfig . parseConfig) cfg
193198
res <- runContract fp c
194199
mapM_ (\(t,f) -> assertBool t $ f res) as
195200

0 commit comments

Comments
 (0)