Skip to content

Commit cc1868e

Browse files
authored
Merge pull request #1441 from gustavo-grieco/dev-sym-exec-fixes
Symbolic execution fixes to make sure no counter example is missed
2 parents e65bd1c + 75d8a22 commit cc1868e

File tree

11 files changed

+119
-166
lines changed

11 files changed

+119
-166
lines changed

flake.nix

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@
5454
(pkgs.haskellPackages.callCabal2nix "hevm" (pkgs.fetchFromGitHub {
5555
owner = "ethereum";
5656
repo = "hevm";
57-
rev = "9982c580ed19b88ebab9744d29d940fd2f0bd8c6";
58-
sha256 = "sha256-B60aJo9EfIaTWuJaN06Wl1Br+rstzUAYGFBE5rX72Fs=";
57+
rev = "87fe0eec5abb69c0b54c097784dfd8712a36de70";
58+
sha256 = "sha256-cgfrP+K5NoXvVPRN6XRnTkdOIJztc/4wrto9nQ/9tnY=";
5959
}) { secp256k1 = pkgs.secp256k1; })
6060
([
6161
pkgs.haskell.lib.compose.dontCheck

lib/Echidna.hs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import System.FilePath ((</>))
1515
import EVM (cheatCode)
1616
import EVM.ABI (AbiValue(AbiAddress))
1717
import EVM.Dapp (dappInfo)
18-
import EVM.Fetch qualified
1918
import EVM.Solidity (BuildOutput(..), Contracts(Contracts), Method(..), Mutability(..), SolcContract(..))
2019
import EVM.Types hiding (Env)
2120

@@ -111,7 +110,7 @@ loadInitialCorpus env = do
111110
mkEnv :: EConfig -> BuildOutput -> [EchidnaTest] -> World -> Maybe SlitherInfo -> IO Env
112111
mkEnv cfg buildOutput tests world slitherInfo = do
113112
codehashMap <- newIORef mempty
114-
chainId <- maybe (pure Nothing) EVM.Fetch.fetchChainIdFrom cfg.rpcUrl
113+
chainId <- maybe (pure Nothing) Onchain.fetchChainIdFrom (Just cfg.rpcUrl)
115114
eventQueue <- newChan
116115
coverageRefInit <- newIORef mempty
117116
coverageRefRuntime <- newIORef mempty

lib/Echidna/Campaign.hs

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -294,27 +294,26 @@ runSymWorker callback vm dict workerId _ name = do
294294

295295
modify' (\ws -> ws { runningThreads = [] })
296296
lift callback
297+
-- We can't do callseq vm' [symTx] because callseq might post the full call sequence as an event
298+
newCoverage <- or <$> mapM (\symTx -> snd <$> callseq vm [symTx]) txs
297299
let methodSignature = unpack method.methodSignature
298-
if not (null partials) || not (null errors) then do
300+
unless newCoverage ( do
301+
unless (null txs) $ error "No new coverage but symbolic execution found valid txs. Something is wrong."
302+
updateTests $ \test -> do
303+
if isOpen test && isAssertionTest test && getAssertionSignature test == methodSignature then
304+
pure $ Just $ test { Test.state = Unsolvable }
305+
else
306+
pure $ Just test
307+
pushWorkerEvent $ SymExecLog ("Symbolic execution finished verifying contract " <> unpack (fromJust name) <> " using a single symbolic transaction."))
308+
309+
when (not (null partials) || not (null errors)) $ do
299310
unless (null errors) $ mapM_ ((pushWorkerEvent . SymExecError) . (\e -> "Error(s) solving constraints produced by method " <> methodSignature <> ": " <> show e)) errors
300311
unless (null partials) $ mapM_ ((pushWorkerEvent . SymExecError) . (\e -> "Partial explored path(s) during symbolic verification of method " <> methodSignature <> ": " <> unpack e)) partials
301312
updateTests $ \test -> do
302-
if isOpen test && isAssertionTest test && getAssertionSignature test == methodSignature then
303-
pure $ Just $ test { Test.state = Passed }
304-
else
305-
pure $ Just test
306-
else do
307-
-- We can't do callseq vm' [symTx] because callseq might post the full call sequence as an event
308-
newCoverage <- or <$> mapM (\symTx -> snd <$> callseq vm [symTx]) txs
309-
310-
unless newCoverage ( do
311-
unless (null txs) $ error "No new coverage but symbolic execution found valid txs. Something is wrong."
312-
updateTests $ \test -> do
313-
if isOpen test && isAssertionTest test && getAssertionSignature test == methodSignature then
314-
pure $ Just $ test { Test.state = Unsolvable }
315-
else
316-
pure $ Just test
317-
pushWorkerEvent $ SymExecLog ("Symbolic execution finished verifying contract " <> unpack (fromJust name) <> " using a single symbolic transaction."))
313+
if isOpen test && isAssertionTest test && getAssertionSignature test == methodSignature then
314+
pure $ Just $ test {Test.state = Passed}
315+
else
316+
pure $ Just test
318317

319318
-- | Run a fuzzing campaign given an initial universe state, some tests, and an
320319
-- optional dictionary to generate calls with. Return the 'Campaign' state once

lib/Echidna/Onchain.hs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
module Echidna.Onchain where
44

55
import Control.Exception (catch)
6-
import Data.Aeson (ToJSON, FromJSON, ToJSONKey(toJSONKey))
6+
import Data.Aeson (ToJSON, FromJSON)
77
import Data.Aeson qualified as JSON
8-
import Data.Aeson.Types (toJSONKeyText)
98
import Data.ByteString (ByteString)
109
import Data.ByteString qualified as BS
1110
import Data.ByteString.UTF8 qualified as UTF8
@@ -25,9 +24,11 @@ import Optics (view)
2524
import System.Directory (createDirectoryIfMissing, doesFileExist)
2625
import System.Environment (lookupEnv)
2726
import System.FilePath ((</>))
27+
import Network.Wreq.Session qualified as Session
2828
import Text.Read (readMaybe)
2929

3030
import EVM (initialContract, bytecode)
31+
import EVM.Effects (defaultConfig)
3132
import EVM.Fetch qualified
3233
import EVM.Solidity (SourceCache(..), SolcContract (..))
3334
import EVM.Types hiding (Env)
@@ -57,16 +58,16 @@ etherscanApiKey = do
5758

5859
-- TODO: temporary solution, handle errors gracefully
5960
safeFetchContractFrom :: EVM.Fetch.BlockNumber -> Text -> Addr -> IO (Maybe Contract)
60-
safeFetchContractFrom rpcBlock rpcUrl addr =
61+
safeFetchContractFrom rpcBlock rpcUrl addr = do
6162
catch
62-
(EVM.Fetch.fetchContractFrom rpcBlock rpcUrl addr)
63+
(EVM.Fetch.fetchContractFrom defaultConfig rpcBlock rpcUrl addr)
6364
(\(_ :: HttpException) -> pure $ Just emptyAccount)
6465

6566
-- TODO: temporary solution, handle errors gracefully
6667
safeFetchSlotFrom :: EVM.Fetch.BlockNumber -> Text -> Addr -> W256 -> IO (Maybe W256)
6768
safeFetchSlotFrom rpcBlock rpcUrl addr slot =
6869
catch
69-
(EVM.Fetch.fetchSlotFrom rpcBlock rpcUrl addr slot)
70+
(EVM.Fetch.fetchSlotFrom defaultConfig rpcBlock rpcUrl addr slot)
7071
(\(_ :: HttpException) -> pure $ Just 0)
7172

7273
data FetchedContractData = FetchedContractData
@@ -76,9 +77,6 @@ data FetchedContractData = FetchedContractData
7677
}
7778
deriving (Generic, ToJSON, FromJSON, Show)
7879

79-
instance ToJSONKey W256 where
80-
toJSONKey = toJSONKeyText (Text.pack . show)
81-
8280
fromFetchedContractData :: FetchedContractData -> Contract
8381
fromFetchedContractData contractData =
8482
(initialContract (RuntimeCode (ConcreteRuntimeCode contractData.runtimeCode)))
@@ -211,3 +209,12 @@ saveCoverageReport env runId = do
211209
[solcContract]
212210
Nothing -> pure ()
213211
Nothing -> pure ()
212+
213+
fetchChainIdFrom :: Maybe Text -> IO (Maybe W256)
214+
fetchChainIdFrom (Just url) = do
215+
sess <- Session.newAPISession
216+
EVM.Fetch.fetchQuery
217+
EVM.Fetch.Latest -- this shouldn't matter
218+
(EVM.Fetch.fetchWithSession url sess)
219+
EVM.Fetch.QueryChainId
220+
fetchChainIdFrom Nothing = pure Nothing

lib/Echidna/SymExec/Common.hs

Lines changed: 41 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,26 @@
11
module Echidna.SymExec.Common where
22

33
import Control.Monad.IO.Unlift (MonadUnliftIO, liftIO)
4-
import Data.ByteString.Lazy qualified as BS
54
import Data.Function ((&))
65
import Data.Map qualified as Map
76
import Data.Maybe (fromMaybe, mapMaybe)
87
import Data.Set (Set)
98
import Data.Set qualified as Set
109
import Data.Text qualified as T
11-
import Data.Vector (toList, fromList)
1210
import GHC.IORef (IORef, readIORef)
1311
import Optics.Core ((.~), (%), (%~))
14-
import EVM.ABI (abiKind, AbiKind(Dynamic), AbiValue(..), AbiType(..), Sig(..), decodeAbiValue)
12+
import EVM.ABI (abiKind, AbiKind(Dynamic), Sig(..), decodeBuf, AbiVals(..))
1513
import EVM.Fetch qualified as Fetch
16-
import EVM (loadContract, resetState, forceLit)
14+
import EVM (loadContract, resetState, symbolify)
1715
import EVM.Effects (TTY, ReadConfig)
18-
import EVM.Solidity (SolcContract(..), Method(..))
16+
import EVM.Solidity (SolcContract(..), SourceCache(..), Method(..), WarningData(..))
1917
import EVM.Solvers (SolverGroup)
20-
import EVM.SymExec (abstractVM, mkCalldata, verifyInputs, VeriOpts(..), checkAssertions)
21-
import EVM.Types (Addr, Frame(..), FrameState(..), VMType(..), EType(..), Expr(..), word256Bytes, Block(..), W256, SMTCex(..), ProofResult(..), Prop(..), Query(..))
22-
import qualified EVM.Types (VM(..), Env(..))
23-
import EVM.Format (formatPartial)
24-
import Control.Monad.ST (stToIO, RealWorld)
18+
import EVM.SymExec (mkCalldata, verifyInputs, VeriOpts(..), checkAssertions, subModel, defaultSymbolicValues)
19+
import EVM.Expr qualified
20+
import EVM.Types (Addr, VMType(..), EType(..), Expr(..), Block(..), W256, SMTCex(..), ProofResult(..), Prop(..), Query(..), forceLit)
21+
import qualified EVM.Types (VM(..))
22+
import EVM.Format (formatPartialDetailed)
23+
import Control.Monad.ST (RealWorld)
2524
import Control.Monad.State.Strict (execState, runStateT)
2625

2726
import Echidna.Types (fromEVM)
@@ -50,31 +49,6 @@ suitableForSymExec m = not $ null m.inputs
5049
&& null (filter (\(_, t) -> abiKind t == Dynamic) m.inputs)
5150
&& not (T.isInfixOf "_no_symexec" m.name)
5251

53-
-- | Sets result to Nothing, and sets gas to ()
54-
vmMakeSymbolic :: W256 -> W256 -> EVM.Types.VM Concrete s -> EVM.Types.VM Symbolic s
55-
vmMakeSymbolic maxTimestampDiff maxNumberDiff vm
56-
= EVM.Types.VM
57-
{ result = Nothing
58-
, state = frameStateMakeSymbolic vm.state
59-
, frames = map frameMakeSymbolic vm.frames
60-
, env = vm.env
61-
, block = blockMakeSymbolic vm.block
62-
, tx = vm.tx
63-
, logs = vm.logs
64-
, traces = vm.traces
65-
, cache = vm.cache
66-
, burned = ()
67-
, iterations = vm.iterations
68-
, constraints = addBlockConstraints maxTimestampDiff maxNumberDiff vm.block vm.constraints
69-
, config = vm.config
70-
, forks = vm.forks
71-
, currentFork = vm.currentFork
72-
, labels = vm.labels
73-
, osEnv = vm.osEnv
74-
, freshVar = vm.freshVar
75-
, exploreDepth = 0
76-
, keccakPreImgs = vm.keccakPreImgs
77-
}
7852

7953
blockMakeSymbolic :: Block -> Block
8054
blockMakeSymbolic b
@@ -93,72 +67,21 @@ addBlockConstraints maxTimestampDiff maxNumberDiff block cs =
9367
senderConstraints :: Set Addr -> [Prop]
9468
senderConstraints as = [foldr (\a b -> POr b (PEq (SymAddr "caller") (LitAddr a))) (PBool False) $ Set.toList as]
9569

96-
frameStateMakeSymbolic :: FrameState Concrete s -> FrameState Symbolic s
97-
frameStateMakeSymbolic fs
98-
= FrameState
99-
{ contract = fs.contract
100-
, codeContract = fs.codeContract
101-
, code = fs.code
102-
, pc = fs.pc
103-
, stack = fs.stack
104-
, memory = fs.memory
105-
, memorySize = fs.memorySize
106-
, calldata = fs.calldata
107-
, callvalue = fs.callvalue
108-
, caller = fs.caller
109-
, gas = ()
110-
, returndata = fs.returndata
111-
, static = fs.static
112-
, overrideCaller = fs.overrideCaller
113-
, resetCaller = fs.resetCaller
114-
}
115-
116-
frameMakeSymbolic :: Frame Concrete s -> Frame Symbolic s
117-
frameMakeSymbolic fr = Frame { context = fr.context, state = frameStateMakeSymbolic fr.state }
118-
119-
-- | Convert a n-bit unsigned integer to a n-bit signed integer.
120-
uintToInt :: W256 -> Integer
121-
uintToInt w = fromIntegral (w - 2 ^ (256 :: Int))
122-
123-
modelToTx :: Addr -> Expr EWord -> Expr EWord -> Method -> Set Addr -> Addr -> ProofResult SMTCex String -> TxOrError
124-
modelToTx dst oldTimestamp oldNumber method senders fallbackSender result =
70+
modelToTx :: Addr -> Expr EWord -> Expr EWord -> Method -> Set Addr -> Addr -> Expr Buf -> ProofResult SMTCex String -> TxOrError
71+
modelToTx dst oldTimestamp oldNumber method senders fallbackSender calldata result =
12572
case result of
12673
Cex cex ->
12774
let
128-
args = zipWith grabArg (snd <$> method.inputs) ["arg" <> T.pack (show n) | n <- [1..] :: [Int]]
129-
130-
grabArg t
131-
= case t of
132-
AbiUIntType _ -> grabNormalArg t
133-
AbiIntType _ -> grabNormalArgInt t
134-
AbiBoolType -> grabNormalArg t
135-
AbiBytesType _ -> grabNormalArg t
136-
AbiAddressType -> grabAddressArg
137-
AbiArrayType n mt -> grabArrayArg n mt
138-
AbiTupleType mt -> grabTupleArg mt
139-
_ -> error "Unexpected ABI type in `modelToTx`"
140-
141-
grabNormalArg argType name
142-
= case Map.lookup (Var name) cex.vars of
143-
Just w ->
144-
decodeAbiValue argType (BS.fromStrict (word256Bytes w))
145-
Nothing -> -- put a placeholder
146-
decodeAbiValue argType (BS.repeat 0)
147-
148-
grabNormalArgInt argType name
149-
= case Map.lookup (Var name) cex.vars of
150-
Just w ->
151-
case argType of
152-
AbiIntType n -> AbiInt n (fromIntegral (uintToInt w))
153-
_ -> error "Expected AbiIntType"
154-
Nothing -> -- put a placeholder
155-
decodeAbiValue argType (BS.repeat 0)
156-
157-
grabAddressArg name = AbiAddress $ fromMaybe 0 $ Map.lookup (SymAddr name) cex.addrs
158-
159-
grabArrayArg nElem memberType name = AbiArray nElem memberType $ fromList [grabArg memberType $ name <> "-a-" <> T.pack (show n) | n <- [0..nElem - 1] :: [Int]]
160-
161-
grabTupleArg memberTypes name = AbiTuple $ fromList [grabArg t $ name <> "-t-" <> T.pack (show n) | (n, t) <- zip ([0..] :: [Int]) (toList memberTypes)]
75+
cd = defaultSymbolicValues $ subModel cex calldata
76+
types = snd <$> method.inputs
77+
argdata = case cd of
78+
Right cd' -> Right $ EVM.Expr.drop 4 (EVM.Expr.simplify cd')
79+
Left e -> Left e
80+
args = case argdata of
81+
Right argdata' -> case decodeBuf types argdata' of
82+
CAbi v -> v
83+
_ -> []
84+
Left _ -> []
16285

16386
src_ = fromMaybe 0 $ Map.lookup (SymAddr "caller") cex.addrs
16487
src = if Set.member src_ senders then src_ else fallbackSender
@@ -222,28 +145,32 @@ getUnknownLogs = mapMaybe (\case
222145
_ -> Nothing)
223146

224147
exploreMethod :: (MonadUnliftIO m, ReadConfig m, TTY m) =>
225-
Method -> SolcContract -> EVM.Types.VM Concrete RealWorld -> Addr -> EConfig -> VeriOpts -> SolverGroup -> Fetch.RpcInfo -> IORef ContractCache -> IORef SlotCache -> m ([TxOrError], PartialsLogs)
148+
Method -> SolcContract -> SourceCache -> EVM.Types.VM Concrete RealWorld -> Addr -> EConfig -> VeriOpts -> SolverGroup -> Fetch.RpcInfo -> IORef ContractCache -> IORef SlotCache -> m ([TxOrError], PartialsLogs)
226149

227-
exploreMethod method contract vm defaultSender conf veriOpts solvers rpcInfo contractCacheRef slotCacheRef = do
228-
--liftIO $ putStrLn ("Exploring: " ++ T.unpack method.methodSignature)
229-
--pushWorkerEvent undefined
230-
calldataSym@(cd, constraints) <- mkCalldata (Just (Sig method.methodSignature (snd <$> method.inputs))) []
150+
exploreMethod method contract sources vm defaultSender conf veriOpts solvers rpcInfo contractCacheRef slotCacheRef = do
151+
calldataSym@(_, constraints) <- mkCalldata (Just (Sig method.methodSignature (snd <$> method.inputs))) []
152+
let
153+
cd = fst calldataSym
231154
let
232155
fetcher = cachedOracle contractCacheRef slotCacheRef solvers rpcInfo
233156
dst = conf.solConf.contractAddr
234-
vmSym = abstractVM calldataSym contract.runtimeCode Nothing False
235-
vmSym' <- liftIO $ stToIO vmSym
236157
vmReset <- liftIO $ snd <$> runStateT (fromEVM resetState) vm
237-
let vm' = vmReset & execState (loadContract (LitAddr dst))
238-
& vmMakeSymbolic conf.txConf.maxTimeDelay conf.txConf.maxBlockDelay
239-
& #constraints %~ (++ constraints ++ (senderConstraints conf.solConf.sender))
240-
& #state % #callvalue .~ TxValue
241-
& #state % #caller .~ SymAddr "caller"
242-
& #state % #calldata .~ cd
243-
& #env % #contracts .~ (Map.union vmSym'.env.contracts vm.env.contracts)
158+
let
159+
vm' = vmReset & execState (loadContract (LitAddr dst))
160+
& #tx % #isCreate .~ False
161+
& #state % #callvalue .~ TxValue
162+
& #state % #caller .~ SymAddr "caller"
163+
& #state % #calldata .~ cd
164+
165+
vm'' = symbolify vm'
166+
& #block %~ blockMakeSymbolic
167+
& #constraints %~ (addBlockConstraints conf.txConf.maxTimeDelay conf.txConf.maxBlockDelay vm'.block)
168+
& #constraints %~ (++ constraints ++ senderConstraints conf.solConf.sender)
169+
244170
-- TODO we might want to switch vm's state.baseState value to to AbstractBase eventually.
245171
-- Doing so might mess up concolic execution.
246-
(_, models, partials) <- verifyInputs solvers veriOpts fetcher vm' (Just $ checkAssertions [0x1])
172+
(_, models, partials) <- verifyInputs solvers veriOpts fetcher vm'' (Just $ checkAssertions [0x1])
247173
let results = map fst models
174+
let warnData = Just $ WarningData contract sources vm'
248175
--liftIO $ mapM_ TIO.putStrLn partials
249-
return (map (modelToTx dst vm.block.timestamp vm.block.number method conf.solConf.sender defaultSender) results, map (formatPartial . fst) partials)
176+
return (map (modelToTx dst vm.block.timestamp vm.block.number method conf.solConf.sender defaultSender cd) results, map (formatPartialDetailed warnData . fst) partials)

0 commit comments

Comments
 (0)