Skip to content

Commit 75d8a22

Browse files
Merge branch 'master' into dev-sym-exec-fixes
2 parents c7ea93e + 4c43c1f commit 75d8a22

File tree

20 files changed

+1481
-58
lines changed

20 files changed

+1481
-58
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ on:
1010

1111
env:
1212
# Tag for cache invalidation
13-
CACHE_VERSION: v9
13+
CACHE_VERSION: v10
1414

1515
jobs:
1616
build:
@@ -23,7 +23,7 @@ jobs:
2323
- os: ubuntu-latest
2424
shell: bash
2525
container: "{\"image\": \"elopeztob/alpine-haskell-stack-echidna:ghc-9.8.4\", \"options\": \"--user 1001\"}"
26-
- os: macos-13 # x86_64 macOS
26+
- os: macos-15-intel # x86_64 macOS
2727
shell: bash
2828
- os: windows-latest
2929
shell: msys2 {0}

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
name: Linux (aarch64)
3030
tuple: aarch64-linux
3131
timeout: 180
32-
- os: macos-13
32+
- os: macos-15-intel
3333
name: macOS (x86_64)
3434
tuple: x86_64-macos
3535
- os: macos-14

lib/Echidna.hs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,24 @@ import Control.Concurrent (newChan)
44
import Control.Monad.Catch (MonadThrow(..))
55
import Control.Monad.ST (RealWorld)
66
import Data.IORef (newIORef)
7-
import Data.List (find)
7+
import Data.List (find, nub)
88
import Data.List.NonEmpty (NonEmpty)
99
import Data.List.NonEmpty qualified as NE
1010
import Data.Map.Strict qualified as Map
11+
import Data.Maybe (mapMaybe)
1112
import Data.Set qualified as Set
1213
import System.FilePath ((</>))
1314

1415
import EVM (cheatCode)
1516
import EVM.ABI (AbiValue(AbiAddress))
1617
import EVM.Dapp (dappInfo)
17-
import EVM.Solidity (BuildOutput(..), Contracts(Contracts))
18+
import EVM.Solidity (BuildOutput(..), Contracts(Contracts), Method(..), Mutability(..), SolcContract(..))
1819
import EVM.Types hiding (Env)
1920

2021
import Echidna.ABI
2122
import Echidna.Onchain as Onchain
2223
import Echidna.Output.Corpus
24+
import Echidna.SourceMapping (findSrcForReal)
2325
import Echidna.SourceAnalysis.Slither
2426
import Echidna.Solidity
2527
import Echidna.SymExec.Symbolic (forceAddr)
@@ -72,22 +74,28 @@ prepareContract cfg solFiles buildOutput selectedContract seed = do
7274

7375
-- deploy contracts
7476
vm <- loadSpecified env mainContract contracts
75-
7677
let
7778
deployedAddresses = Set.fromList $ AbiAddress . forceAddr <$> Map.keys vm.env.contracts
7879
constants = enhanceConstants slitherInfo
7980
<> timeConstants
8081
<> extremeConstants
8182
<> staticAddresses solConf
8283
<> deployedAddresses
83-
84+
deployedSolcContracts = nub $ mapMaybe (findSrcForReal env.dapp) $ Map.elems vm.env.contracts
85+
nonViewPureSigs = concatMap (mapMaybe (\ (Method {name, inputs, mutability}) ->
86+
case mutability of
87+
View -> Nothing
88+
Pure -> Nothing
89+
Payable -> Just (name, map snd inputs)
90+
NonPayable -> Just (name, map snd inputs))
91+
. Map.elems . (\ (SolcContract {abiMap}) -> abiMap)) deployedSolcContracts
8492
dict = mkGenDict env.cfg.campaignConf.dictFreq
8593
-- make sure we don't use cheat codes to form fuzzing call sequences
8694
(Set.delete (AbiAddress $ forceAddr cheatCode) constants)
8795
Set.empty
8896
seed
8997
(returnTypes contracts)
90-
98+
nonViewPureSigs
9199
pure (vm, env, dict)
92100

93101
loadInitialCorpus :: Env -> IO [(FilePath, [Tx])]

lib/Echidna/ABI.hs

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module Echidna.ABI where
22

3-
import Control.Monad (liftM2, liftM3, foldM, replicateM)
3+
import Control.Monad (liftM2, liftM3, foldM, replicateM, zipWithM)
44
import Control.Monad.Random.Strict (MonadRandom, join, getRandom, getRandoms, getRandomR, uniform, fromList)
55
import Control.Monad.Random.Strict qualified as Random
66
import Data.Binary.Put (runPut, putWord32be)
@@ -121,6 +121,8 @@ data GenDict = GenDict
121121
-- ^ Return types of any methods we scrape return values from
122122
, dictValues :: !(Set W256)
123123
-- ^ A set of int/uint constants for better performance
124+
, callbackSigs :: ![SolSignature]
125+
-- ^ A list of callback signatures (for generating random callbacks)
124126
}
125127

126128
hashMapBy
@@ -142,6 +144,7 @@ mkGenDict
142144
-> Set SolCall -- ^ A list of complete 'SolCall's to mutate
143145
-> Int -- ^ A default seed
144146
-> (Text -> Maybe AbiType) -- ^ A return value typing rule
147+
-> [SolSignature]
145148
-> GenDict
146149
mkGenDict mutationChance abiValues solCalls seed typingRule =
147150
GenDict mutationChance
@@ -152,7 +155,7 @@ mkGenDict mutationChance abiValues solCalls seed typingRule =
152155
(mkDictValues abiValues)
153156

154157
emptyDict :: GenDict
155-
emptyDict = mkGenDict 0 Set.empty Set.empty 0 (const Nothing)
158+
emptyDict = mkGenDict 0 Set.empty Set.empty 0 (const Nothing) []
156159

157160
mkDictValues :: Set AbiValue -> Set W256
158161
mkDictValues =
@@ -382,31 +385,50 @@ pregenAbiAdds = map (AbiAddress . fromIntegral) pregenAdds
382385
-- | Synthesize a random 'AbiValue' given its 'AbiType'. Requires a dictionary.
383386
-- Only produce lists with number of elements in the range [1, 32]
384387
genAbiValueM :: MonadRandom m => GenDict -> AbiType -> m AbiValue
385-
genAbiValueM genDict = genWithDict genDict genDict.constants $ \case
386-
AbiUIntType n -> fixAbiUInt n . fromInteger <$> getRandomUint n
387-
AbiIntType n -> fixAbiInt n . fromInteger <$> getRandomInt n
388-
AbiAddressType -> rElem $ NE.fromList pregenAbiAdds
389-
AbiBoolType -> AbiBool <$> getRandom
390-
AbiBytesType n -> AbiBytes n . BS.pack . take n <$> getRandoms
391-
AbiBytesDynamicType -> liftM2 (\n -> AbiBytesDynamic . BS.pack . take n)
392-
(getRandomR (1, 32)) getRandoms
393-
AbiStringType -> liftM2 (\n -> AbiString . BS.pack . take n)
394-
(getRandomR (1, 32)) getRandoms
395-
AbiArrayDynamicType t -> fmap (AbiArrayDynamic t) $ getRandomR (1, 32)
396-
>>= flip V.replicateM (genAbiValueM genDict t)
397-
AbiArrayType n t -> AbiArray n t <$> V.replicateM n (genAbiValueM genDict t)
398-
AbiTupleType v -> AbiTuple <$> traverse (genAbiValueM genDict) v
399-
AbiFunctionType -> liftM2 (\n -> AbiString . BS.pack . take n)
400-
(getRandomR (1, 32)) getRandoms
388+
genAbiValueM genDict = genAbiValueM' genDict "" 0
389+
390+
genAbiValueM' :: MonadRandom m => GenDict -> Text -> Int -> AbiType -> m AbiValue
391+
genAbiValueM' genDict funcName depth t =
392+
let go = \case
393+
AbiUIntType n -> fixAbiUInt n . fromInteger <$> getRandomUint n
394+
AbiIntType n -> fixAbiInt n . fromInteger <$> getRandomInt n
395+
AbiAddressType -> rElem $ NE.fromList pregenAbiAdds
396+
AbiBoolType -> AbiBool <$> getRandom
397+
AbiBytesType n -> AbiBytes n . BS.pack . take n <$> getRandoms
398+
AbiBytesDynamicType ->
399+
let
400+
filteredSigs = filter ((/= funcName) . fst) genDict.callbackSigs
401+
in if null filteredSigs || depth >= 2 then
402+
liftM2 (\n -> AbiBytesDynamic . BS.pack . take n)
403+
(getRandomR (1, 32)) getRandoms
404+
else
405+
join $ Random.weighted
406+
[ (do
407+
sig@(_, types) <- uniform filteredSigs
408+
params <- V.fromList <$> mapM (genAbiValueM' genDict "" $ depth + 1) types
409+
pure $ AbiBytesDynamic (abiCalldata (encodeSig sig) params), 9)
410+
, (liftM2 (\n -> AbiBytesDynamic . BS.pack . take n)
411+
(getRandomR (1, 8)) getRandoms, 1)
412+
]
413+
AbiStringType -> liftM2 (\n -> AbiString . BS.pack . take n)
414+
(getRandomR (1, 32)) getRandoms
415+
AbiArrayDynamicType t' -> fmap (AbiArrayDynamic t') $ getRandomR (1, 32)
416+
>>= flip V.replicateM (genAbiValueM' genDict funcName (depth + 1) t')
417+
AbiArrayType n t' -> AbiArray n t' <$> V.replicateM n (genAbiValueM' genDict funcName (depth + 1) t')
418+
AbiTupleType v -> AbiTuple <$> traverse (genAbiValueM' genDict funcName (depth + 1)) v
419+
AbiFunctionType -> liftM2 (\n -> AbiString . BS.pack . take n)
420+
(getRandomR (1, 32)) getRandoms
421+
in genWithDict genDict genDict.constants go t
401422

402423
-- | Given a 'SolSignature', generate a random 'SolCall' with that signature,
403424
-- possibly with a dictionary.
404425
genAbiCallM :: MonadRandom m => GenDict -> SolSignature -> m SolCall
405-
genAbiCallM genDict abi = do
426+
genAbiCallM genDict (name, types) = do
427+
let genVals = zipWithM (flip (genAbiValueM' genDict name)) types (repeat 0)
406428
solCall <- genWithDict genDict
407429
genDict.wholeCalls
408-
(traverse $ traverse (genAbiValueM genDict))
409-
abi
430+
(const ((name,) <$> genVals))
431+
(name, types)
410432
mutateAbiCall solCall
411433

412434
-- | Given a list of 'SolSignature's, generate a random 'SolCall' for one,

lib/Echidna/Config.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ instance FromJSON EConfigWithUsage where
9898
<*> v ..:? "coverageDir" ..!= Nothing
9999
<*> v ..:? "mutConsts" ..!= defaultMutationConsts
100100
<*> v ..:? "coverageFormats" ..!= [Txt,Html,Lcov]
101+
<*> v ..:? "coverageExcludes" ..!= []
101102
<*> v ..:? "workers"
102103
<*> v ..:? "server"
103104
<*> v ..:? "symExec" ..!= False

lib/Echidna/Output/Foundry.hs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
{-# LANGUAGE RecordWildCards #-}
3+
{-# LANGUAGE TemplateHaskell #-}
4+
5+
module Echidna.Output.Foundry (foundryTest) where
6+
7+
import Data.Aeson (Value(..), object, (.=))
8+
import Data.List (elemIndex, nub)
9+
import Data.Maybe (fromMaybe, mapMaybe)
10+
import Data.Text (Text, unpack)
11+
import Data.Text.Encoding (decodeUtf8)
12+
import qualified Data.Text.Lazy as TL
13+
import Data.Text.Lazy (fromStrict)
14+
import Data.Vector as V hiding ((++), map, zipWith, elemIndex, mapMaybe)
15+
import EVM.ABI (AbiValue(..))
16+
import EVM.Types (W256, Addr)
17+
import Numeric (showHex)
18+
import Text.Mustache (Template, substituteValue, toMustache)
19+
import Text.Mustache.Compile (embedTemplate)
20+
21+
import Echidna.Types.Test (EchidnaTest(..), TestType(..))
22+
import Echidna.Types.Tx (Tx(..), TxCall(..))
23+
24+
template :: Template
25+
template = $(embedTemplate ["lib/Echidna/Output/assets"] "foundry.mustache")
26+
27+
-- | Generate a Foundry test from an EchidnaTest result.
28+
foundryTest :: Maybe Text -> EchidnaTest -> TL.Text
29+
foundryTest mContractName test =
30+
case test.testType of
31+
AssertionTest{} ->
32+
let testData = createTestData mContractName test
33+
in fromStrict $ substituteValue template (toMustache testData)
34+
_ -> ""
35+
36+
-- | Create an Aeson Value from test data for the Mustache template.
37+
createTestData :: Maybe Text -> EchidnaTest -> Value
38+
createTestData mContractName test =
39+
let
40+
senders = nub $ map (.src) test.reproducer
41+
actors = zipWith actorObject senders [1..]
42+
repro = mapMaybe (foundryTx senders) test.reproducer
43+
cName = fromMaybe "YourContract" mContractName
44+
in
45+
object
46+
[ "testName" .= ("Test" :: Text)
47+
, "contractName" .= cName
48+
, "actors" .= actors
49+
, "reproducer" .= repro
50+
]
51+
52+
-- | Create a JSON object for an actor.
53+
actorObject :: Addr -> Int -> Value
54+
actorObject sender i = object
55+
[ "name" .= ("USER" ++ show i :: String)
56+
, "address" .= formatAddr sender
57+
]
58+
59+
-- | Format an address for Solidity.
60+
formatAddr :: Addr -> String
61+
formatAddr addr = "address(0x" ++ showHex (fromIntegral addr :: W256) "" ++ ")"
62+
63+
-- | Generate a single transaction line for the reproducer.
64+
foundryTx :: [Addr] -> Tx -> Maybe Value
65+
foundryTx senders tx =
66+
case tx.call of
67+
SolCall (name, args) ->
68+
let
69+
(time, blocks) = tx.delay
70+
senderName =
71+
case elemIndex tx.src senders of
72+
Just i -> "USER" ++ show (i + 1)
73+
Nothing -> formatAddr tx.src
74+
prelude =
75+
(if time > 0 || blocks > 0 then " _delay(" ++ show time ++ ", " ++ show blocks ++ ");\n" else "") ++
76+
" _setUpActor(" ++ senderName ++ ");"
77+
call = " Target." ++ unpack name ++ "(" ++ foundryArgs (map abiValueToString args) ++ ");"
78+
in Just $ object ["prelude" .= prelude, "call" .= call]
79+
_ -> Nothing
80+
81+
-- | Format arguments for a Solidity call.
82+
foundryArgs :: [String] -> String
83+
foundryArgs [] = ""
84+
foundryArgs [x] = x
85+
foundryArgs (x:xs) = x ++ ", " ++ foundryArgs xs
86+
87+
-- | Convert an AbiValue to its string representation for Solidity.
88+
abiValueToString :: AbiValue -> String
89+
abiValueToString (AbiUInt _ w) = show w
90+
abiValueToString (AbiInt _ w) = show w
91+
abiValueToString (AbiAddress a) = "address(0x" ++ showHex (fromIntegral a :: W256) "" ++ ")"
92+
abiValueToString (AbiBool b) = if b then "true" else "false"
93+
abiValueToString (AbiBytes _ bs) = "hex\"" ++ unpack (decodeUtf8 bs) ++ "\""
94+
abiValueToString (AbiString s) = show s
95+
abiValueToString (AbiTuple vs) = "(" ++ foundryArgs (map abiValueToString (V.toList vs)) ++ ")"
96+
abiValueToString _ = ""

0 commit comments

Comments
 (0)