make generation actually work

pull/181/head
JP Smith 6 years ago
parent b38f6bc6e4
commit 9ff70f4ee7
  1. 13
      examples/solidity/basic/constants.sol
  2. 9
      lib/Echidna/ABI.hs
  3. 40
      lib/Echidna/Solidity.hs
  4. 8
      src/Main.hs
  5. 2
      src/test/Spec.hs

@ -0,0 +1,13 @@
pragma solidity ^0.4.24;
contract Constants {
bool found = false;
function find(int i) public {
if (i == 1337) {found = true;}
}
function echidna_found() public view returns (bool) {
return(!found);
}
}

@ -5,7 +5,6 @@
module Echidna.ABI where
import Control.Applicative ((<**>))
import Control.Monad.Catch (Exception, MonadThrow(..))
import Control.Monad.Reader.Class (MonadReader, asks)
import Control.Monad.Random.Strict
@ -223,9 +222,9 @@ mutateAbiCall = traverse $ traverse mutateAbiValue
genWithDict :: (Eq a, Hashable a, MonadReader x m, Has GenConf x, MonadRandom m, MonadThrow m)
=> (GenConf -> HashMap a [b]) -> (a -> m b) -> a -> m b
genWithDict f g t = asks getter >>= \c -> do
useD <- (pSynthA c <) <$> getRandom
g t <**> case (M.lookup t (f c), useD) of (Just l@(_:_), True) -> const <$> rElem "" l
_ -> pure id
useD <- (pSynthA c >) <$> getRandom
case (M.lookup t (f c), useD) of (Just l@(_:_), True) -> rElem "" l
_ -> g t
-- | Given an 'AbiType', generate a random 'AbiValue' of that type, possibly with a dictionary.
genAbiValueM :: (MonadReader x m, Has GenConf x, MonadRandom m, MonadThrow m) => AbiType -> m AbiValue
@ -233,7 +232,7 @@ genAbiValueM = genWithDict constants genAbiValue
-- | Given a 'SolSignature', generate a random 'SolCalls' with that signature, possibly with a dictionary.
genAbiCallM :: (MonadReader x m, Has GenConf x, MonadRandom m, MonadThrow m) => SolSignature -> m SolCall
genAbiCallM = genWithDict wholeCalls genAbiCall
genAbiCallM = genWithDict wholeCalls (traverse $ traverse genAbiValueM)
-- | Given a list of 'SolSignature's, generate a random 'SolCall' for one, possibly with a dictionary.
genInteractionsM :: (MonadReader x m, Has GenConf x, MonadRandom m, MonadThrow m)

@ -16,7 +16,7 @@ import Control.Monad.State.Strict (execStateT)
import Data.Aeson (Value(..))
import Data.Foldable (toList)
import Data.Has (Has(..))
import Data.List (find, partition, stripPrefix)
import Data.List (find, nub, partition)
import Data.Maybe (isNothing, mapMaybe)
import Data.Monoid ((<>))
import Data.Text (Text, isPrefixOf, pack, unpack)
@ -89,14 +89,16 @@ contracts fp = do
readSolc =<< writeSystemTempFile ""
=<< readCreateProcess (proc "solc" $ usual <> words a) {std_err = stderr} ""
-- | Given a file, an optional name and a list of all the 'SolcContract's in a file, try to load the
-- specified contract into a 'VM' usable for Echidna testing and extract an ABI and list of tests.
-- Throws exceptions if anything returned doesn't look usable for Echidna
-- | Given an optional contract name and a list of 'SolcContract's, try to load the specified
-- contract, or, if not provided, the first contract in the list, into a 'VM' usable for Echidna
-- testing and extract an ABI and list of tests. Throws exceptions if anything returned doesn't look
-- usable for Echidna. NOTE: Contract names passed to this function should be prefixed by the
-- filename their code is in, plus a colon.
loadSpecified :: (MonadIO m, MonadThrow m, MonadReader x m, Has SolConf x)
=> FilePath -> Maybe Text -> [SolcContract] -> m (VM, [SolSignature], [Text])
loadSpecified fp name cs = let ensure l e = if l == mempty then throwM e else pure () in do
=> Maybe Text -> [SolcContract] -> m (VM, [SolSignature], [Text])
loadSpecified name cs = let ensure l e = if l == mempty then throwM e else pure () in do
-- Pick contract to load
c <- choose cs $ ((pack fp <> ":") <>) <$> name
c <- choose cs name
q <- view (hasLens . quiet)
liftIO $ do
when (isNothing name && length cs > 1) $
@ -122,9 +124,14 @@ loadSpecified fp name cs = let ensure l e = if l == mempty then throwM e else pu
choose _ (Just n) = maybe (throwM $ ContractNotFound n) pure $
find ((n ==) . view contractName) cs
-- | 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
-- the first contract in the file. Take said contract and return an initial VM state with it loaded,
-- its ABI (as 'SolSignature's), and the names of its Echidna tests. NOTE: unlike 'loadSpecified',
-- contract names passed here don't need the file they occur in specified.
loadSolidity :: (MonadIO m, MonadThrow m, MonadReader x m, Has SolConf x)
=> FilePath -> Maybe Text -> m (VM, [SolSignature], [Text])
loadSolidity fp name = contracts fp >>= loadSpecified fp name
loadSolidity fp name = contracts fp >>= loadSpecified ((pack fp <>) <$> name)
-- | Given the results of 'loadSolidity', assuming a single-contract test, get everything ready
-- for running a 'Campaign' against the tests found.
@ -141,23 +148,20 @@ loadSolTests fp name = loadSolidity fp name >>= prepareForTest
-- | Given a list of 'SolcContract's, try to parse out string and integer literals
extractConstants :: [SolcContract] -> [AbiValue]
extractConstants = concatMap $ getConstants . view contractAst where
extractConstants = nub . concatMap (getConstants . view contractAst) where
getConstants :: Value -> [AbiValue]
getConstants (Object o) = concat . mapMaybe fromPair $ M.toList o
getConstants (Array a) = concatMap getConstants a
getConstants _ = []
fromPair ("type", (String s)) = let split = words $ unpack s in case split of
"int_const" : i : _ -> ints <$> readMaybe i
"literal_string" : l : _ -> (strs "") <$> stripPrefix "\\\"" l
-- How does the solidity AST work? No one really knows, this is my best guess
fromPair ("type", String s) = let split = words $ unpack s in case split of
"int_const" : i : _ -> ints <$> readMaybe i
"literal_string" : l : _ -> strs <$> BS.stripSuffix "\"" (BS.drop 1 $ BS.pack l)
_ -> Nothing
fromPair _ = Nothing
fromPair (_, o) = Just $ getConstants o
ints :: Integer -> [AbiValue]
ints n = let l f = f <$> [8,16..256] <*> [fromIntegral n] in l AbiInt ++ l AbiUInt
strs :: String -> String -> [AbiValue]
strs _ "" = []
strs x (y : '\\':'"':"") = let s = reverse $ y : x in
[AbiString, AbiBytes (length s), AbiBytesDynamic] <&> ($ (BS.pack s))
strs x (y : ys) = strs (y : x) ys
strs s = [AbiString, AbiBytes (BS.length s), AbiBytesDynamic] <&> ($ s)

@ -38,9 +38,9 @@ main :: IO ()
main = do (Options f c cov conf) <- execParser opts
cfg <- maybe (pure defaultConfig) parseConfig conf
cs <- runReaderT (contracts f) cfg
Campaign r _ <- runReaderT (loadSpecified f (pack <$> c) cs >>= prepareForTest >>=
\(v,w,ts) -> ui v w ts) $ cfg
& cConf %~ (if cov then \k -> k {knownCoverage = Just mempty} else id)
& gConf .~ mkConf 0.15 (extractConstants cs) []
Campaign r _ <- runReaderT (loadSpecified (pack . (f ++) . (':' :) <$> c) cs
>>= prepareForTest >>= \(v,w,ts) -> ui v w ts) $ cfg
& cConf %~ (if cov then \k -> k {knownCoverage = Just mempty} else id)
& gConf .~ mkConf 0.15 (extractConstants cs) []
if any (/= Passed) $ snd <$> r then exitWith $ ExitFailure 1
else exitSuccess

@ -66,6 +66,8 @@ integrationTests = testGroup "Solidity Integration Testing"
, ("echidna_all_sender didn't shrink optimally", solvedLen 3 "echidna_all_sender")
] ++ (["s1", "s2", "s3"] <&> \n ->
("echidna_all_sender solved without " ++ unpack n, solvedWith (n, []) "echidna_all_sender"))
, testContract "basic/constants.sol" Nothing
[ ("echidna_found failed (didn't find constant)", solved "echidna_found") ]
]
testContract :: FilePath -> Maybe FilePath -> [(String, Campaign -> Bool)] -> TestTree

Loading…
Cancel
Save