Skip to content

Commit 0b35c32

Browse files
merge with master
2 parents 0559648 + cde590f commit 0b35c32

File tree

16 files changed

+349
-61
lines changed

16 files changed

+349
-61
lines changed

CHANGELOG.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,32 @@
11
## Unreleased
2-
* Dropped Etheno support (#1402)
2+
3+
## 2.3.1
4+
5+
* Fixes for Foundry reproducer test generation (#1484, #1514)
6+
* Fix SSE getting stuck at the end of execution (#1493)
7+
* Fix gas underreporting on reverted transactions (#1503)
8+
* Onchain coverage improvements with Sourcify support as an alternative to Etherscan (#1504, #1511)
9+
* New `disableOnchainSources` option and `--disable-onchain-sources` CLI flag
10+
* FFI support now handles commands that output an extra newline (#1507)
11+
* Updated hevm to `release/0.57.0` (#1515)
12+
* Newer nixpkgs and compilation fixes for GHC 9.10 (#1500, #1501)
13+
14+
## 2.3.0
15+
16+
* Symbolic execution integration with fuzzing: verification mode for stateless functions and symExec mode combining fuzzing with symbolic execution (#1394)
17+
* Support for Bitwuzla, cvc5, and Z3 solvers (#1421, #1425)
18+
* Foundry test generation for discovered bugs (standalone reproducers for failed assertions) (#1437)
19+
* Improved HTML coverage reporting (#1415) with configurable `coverageDir` option (#1428)
20+
* Enhanced failure diagnostics: all events from all transactions in the sequence are now displayed (#1475)
21+
* Deployment errors now show full execution trace (#1466)
22+
* Shrinking status logged during minimization (#1454)
23+
* Dictionary extraction from tuple elements (#1406)
24+
* Enhanced callback encoding for multicall-style interactions (#1444)
25+
* Docker image now includes Foundry, Z3, and Bitwuzla (#1422, #1469)
26+
* `rtsopts` enabled on release builds (#1457)
27+
* Dropped Etheno support and `initialize` configuration (#1402)
328
* Dropped `estimateGas` support and auxiliary code (#1403)
29+
* Dropped `symExecConcolic` option (#1394)
430

531
## 2.2.7
632

flake.nix

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,10 @@
6060

6161
hevm = pkgs: pkgs.lib.pipe
6262
(pkgs.haskellPackages.callCabal2nix "hevm" (pkgs.fetchFromGitHub {
63-
owner = "ethereum";
63+
owner = "argotorg";
6464
repo = "hevm";
65-
rev = "9ba5e52fc7ec7ae6f7f3a25d5ee426625d2aa9d3";
66-
sha256 = "sha256-5ZWsXtmZsMw2el4cuR9T+qrySTra6Lcaty/RrLOQ2hU=";
65+
rev = "release/0.57.0";
66+
sha256 = "sha256-Fn/u6u5euZ+khabqdOw7N29le29XCnxbOdSZOit+XXk=";
6767
}) { secp256k1 = pkgs.secp256k1; })
6868
([
6969
pkgs.haskell.lib.compose.dontCheck

lib/Echidna.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ instance ReadConfig IO where
122122
mkEnv :: EConfig -> BuildOutput -> [EchidnaTest] -> World -> Maybe SlitherInfo -> IO Env
123123
mkEnv cfg buildOutput tests world slitherInfo = do
124124
codehashMap <- newIORef mempty
125-
chainId <- maybe (pure Nothing) Onchain.fetchChainIdFrom (Just cfg.rpcUrl)
125+
chainId <- Onchain.fetchChainIdFrom cfg.rpcUrl
126126
eventQueue <- newChan
127127
bus <- newBroadcastTChanIO
128128
coverageRefInit <- newIORef mempty

lib/Echidna/Config.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ instance FromJSON EConfigWithUsage where
7272
<*> v ..:? "rpcBlock"
7373
<*> v ..:? "etherscanApiKey"
7474
<*> v ..:? "projectName"
75+
<*> v ..:? "disableOnchainSources" ..!= False
7576
where
7677
useKey k = modify' $ Set.insert k
7778
x ..:? k = useKey k >> lift (x .:? k)

lib/Echidna/Exec.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ execTxWith executeTx tx = do
160160
let process = (P.proc cmd args) { P.env = Just mergedEnv }
161161
(_, stdout, _) <- liftIO $ P.readCreateProcessWithExitCode process ""
162162
let encodedResponse = encodeAbiValue $
163-
AbiTuple (V.fromList [AbiBytesDynamic . hexText . T.pack $ stdout])
163+
AbiTuple (V.fromList [AbiBytesDynamic . hexText . T.strip . T.pack $ stdout])
164164
fromEVM (continuation encodedResponse)
165165
runFully
166166

lib/Echidna/Onchain.hs

Lines changed: 91 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,17 @@ module Echidna.Onchain
1111
where
1212

1313
import Control.Concurrent.MVar (readMVar)
14-
import Control.Exception (catch)
14+
import Control.Exception (catch, SomeException)
1515
import Control.Monad (when, forM_)
1616
import Data.ByteString qualified as BS
1717
import Data.ByteString.UTF8 qualified as UTF8
1818
import Data.Map qualified as Map
19-
import Data.Maybe (isJust, fromJust)
19+
import Data.Maybe (isJust, fromJust, fromMaybe)
20+
import Data.Sequence (Seq)
2021
import Data.Text (Text)
2122
import Data.Text qualified as Text
2223
import Data.Vector qualified as Vector
2324
import Data.Word (Word64)
24-
import Etherscan qualified
2525
import Network.HTTP.Simple (HttpException)
2626
import Network.Wreq.Session qualified as Session
2727
import Optics (view)
@@ -32,9 +32,12 @@ import Text.Read (readMaybe)
3232
import EVM (bytecode)
3333
import EVM.Effects (defaultConfig)
3434
import EVM.Fetch qualified
35-
import EVM.Solidity (SourceCache(..), SolcContract (..))
35+
import EVM.Solidity (SourceCache(..), SolcContract(..), SrcMap, makeSrcMaps)
3636
import EVM.Types hiding (Env)
3737

38+
import Echidna.Onchain.Etherscan qualified as Etherscan
39+
import Echidna.Onchain.Sourcify qualified as Sourcify
40+
import Echidna.Onchain.Types (SourceData(..))
3841
import Echidna.Output.Source (saveCoverages)
3942
import Echidna.SymExec.Symbolic (forceBuf)
4043
import Echidna.Types.Campaign (CampaignConf(..))
@@ -87,42 +90,91 @@ safeFetchSlotFrom session rpcBlock rpcUrl addr slot =
8790
-- code fetched from the outside
8891
externalSolcContract :: Env -> String -> Addr -> Contract -> IO (Maybe (SourceCache, SolcContract))
8992
externalSolcContract env explorerUrl addr c = do
90-
case env.cfg.etherscanApiKey of
93+
let runtimeCode = forceBuf $ fromJust $ view bytecode c
94+
95+
-- Try Sourcify first (if chainId available)
96+
sourcifyResult <- case env.chainId of
97+
Just chainId -> do
98+
putStr $ "Fetching source for " <> show addr <> " from Sourcify... "
99+
Sourcify.fetchContractSource chainId addr
100+
Nothing -> pure Nothing
101+
102+
-- If Sourcify fails, try Etherscan (only if API key exists)
103+
sourceData <- case sourcifyResult of
104+
Just sd -> do
105+
putStrLn "Success!"
106+
pure (Just sd)
107+
Nothing -> do
108+
putStrLn "Failed!"
109+
case env.cfg.etherscanApiKey of
110+
Nothing -> do
111+
putStrLn "Skipping Etherscan (no API key configured)"
112+
pure Nothing
113+
Just _ -> do
114+
putStr $ "Fetching source for " <> show addr <> " from Etherscan... "
115+
result <- Etherscan.fetchContractSourceData
116+
env.chainId
117+
env.cfg.etherscanApiKey
118+
explorerUrl
119+
addr
120+
maybe (putStrLn "Failed!") (const $ putStrLn "Success!") result
121+
pure result
122+
123+
-- Convert to SolcContract
124+
case sourceData of
125+
Just sd -> buildSolcContract runtimeCode sd
91126
Nothing -> pure Nothing
92-
Just _ -> do
93-
let runtimeCode = forceBuf $ fromJust $ view bytecode c
94-
putStr $ "Fetching Solidity source for contract at address " <> show addr <> "... "
95-
srcRet <- Etherscan.fetchContractSource env.chainId env.cfg.etherscanApiKey addr
96-
putStrLn $ if isJust srcRet then "Success!" else "Error!"
97-
putStr $ "Fetching Solidity source map for contract at address " <> show addr <> "... "
98-
srcmapRet <- Etherscan.fetchContractSourceMap explorerUrl addr
99-
putStrLn $ if isJust srcmapRet then "Success!" else "Error!"
100-
pure $ do
101-
src <- srcRet
102-
(_, srcmap) <- srcmapRet
103-
let
104-
files = Map.singleton 0 (show addr, UTF8.fromString src.code)
105-
sourceCache = SourceCache
106-
{ files
107-
, lines = Vector.fromList . BS.split 0xa . snd <$> files
108-
, asts = mempty
109-
}
110-
solcContract = SolcContract
111-
{ runtimeCode = runtimeCode
112-
, creationCode = mempty
113-
, runtimeCodehash = keccak' runtimeCode
114-
, creationCodehash = keccak' mempty
115-
, runtimeSrcmap = mempty
116-
, creationSrcmap = srcmap
117-
, contractName = src.name
118-
, constructorInputs = [] -- error "TODO: mkConstructor abis TODO"
119-
, abiMap = mempty -- error "TODO: mkAbiMap abis"
120-
, eventMap = mempty -- error "TODO: mkEventMap abis"
121-
, errorMap = mempty -- error "TODO: mkErrorMap abis"
122-
, storageLayout = Nothing
123-
, immutableReferences = mempty
124-
}
125-
pure (sourceCache, solcContract)
127+
128+
-- | Build SolcContract and SourceCache from SourceData
129+
buildSolcContract :: BS.ByteString -> SourceData -> IO (Maybe (SourceCache, SolcContract))
130+
buildSolcContract runtimeCode sd = do
131+
-- Build SourceCache from multiple source files
132+
let sourcesList = Map.toList sd.sourceFiles
133+
filesMap = Map.fromList $ zip [0..]
134+
(fmap (\(path, content) -> (Text.unpack path, UTF8.fromString $ Text.unpack content)) sourcesList)
135+
sourceCache = SourceCache
136+
{ files = filesMap
137+
, lines = Vector.fromList . BS.split 0xa . snd <$> filesMap
138+
, asts = mempty
139+
}
140+
141+
-- Parse source maps safely
142+
runtimeSrcmap <- case sd.runtimeSrcMap of
143+
Just sm -> makeSrcMapsSafe sm
144+
Nothing -> pure mempty
145+
146+
creationSrcmap <- case sd.creationSrcMap of
147+
Just sm -> makeSrcMapsSafe sm
148+
Nothing -> pure mempty
149+
150+
-- Build ABI maps
151+
-- TODO: Need mkAbiMap, mkEventMap, mkErrorMap to be exported from hevm
152+
-- For now, we keep them as mempty but at least we have the ABI data available
153+
let (abiMap', eventMap', errorMap') = (mempty, mempty, mempty)
154+
155+
let solcContract = SolcContract
156+
{ runtimeCode = runtimeCode
157+
, creationCode = mempty
158+
, runtimeCodehash = keccak' runtimeCode
159+
, creationCodehash = keccak' mempty
160+
, runtimeSrcmap = runtimeSrcmap
161+
, creationSrcmap = creationSrcmap
162+
, contractName = sd.contractName
163+
, constructorInputs = []
164+
, abiMap = abiMap'
165+
, eventMap = eventMap'
166+
, errorMap = errorMap'
167+
, storageLayout = Nothing
168+
, immutableReferences = fromMaybe mempty sd.immutableRefs
169+
}
170+
171+
pure $ Just (sourceCache, solcContract)
172+
173+
-- | Safe wrapper for makeSrcMaps to prevent crashes
174+
makeSrcMapsSafe :: Text.Text -> IO (Seq SrcMap)
175+
makeSrcMapsSafe txt =
176+
catch (pure $ fromMaybe mempty $! makeSrcMaps txt)
177+
(\(_ :: SomeException) -> pure mempty)
126178

127179

128180
saveCoverageReport :: Env -> Int -> IO ()
Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
module Etherscan
2-
( SourceCode(..)
1+
module Echidna.Onchain.Etherscan
2+
( fetchContractSourceData
33
, getBlockExplorerUrl
4-
, fetchContractSource
5-
, fetchContractSourceMap
64
)
75
where
86

@@ -26,6 +24,8 @@ import Text.XML.Cursor (attributeIs, content, element, fromDocument, ($//), (&//
2624
import EVM.Solidity (makeSrcMaps, SrcMap)
2725
import EVM.Types (Addr, W256)
2826

27+
import Echidna.Onchain.Types (SourceData(..))
28+
2929
data SourceCode = SourceCode
3030
{ name :: Text
3131
, code :: String
@@ -132,9 +132,8 @@ getBlockExplorerUrl maybeChainId = do
132132
Just url -> pure $ T.unpack url
133133

134134
-- | Unfortunately, Etherscan doesn't expose source maps in the JSON API.
135-
-- This function scrapes it from the HTML. Return a tuple where the first element
136-
-- is raw srcmap in text format and the second element is a parsed map.
137-
fetchContractSourceMap :: String -> Addr -> IO (Maybe (Text, Seq SrcMap))
135+
-- This function scrapes it from the HTML. Return the raw srcmap in text format
136+
fetchContractSourceMap :: String -> Addr -> IO (Maybe Text)
138137
fetchContractSourceMap baseUrl addr = do
139138
-- Scrape HTML from block explorer
140139
url <- parseRequest $ baseUrl <> "/address/" <> show addr
@@ -150,11 +149,34 @@ fetchContractSourceMap baseUrl addr = do
150149
-- combine with raw srcmap to return so it is easier to cache
151150
case catMaybes $ zipWith (\x -> fmap (x,)) candidates parsedCandidates of
152151
[] -> pure Nothing
153-
srcmap:_ -> pure (Just srcmap)
152+
srcmap:_ -> pure (Just $ fst srcmap)
154153

155154
-- | Calling makeSrcMaps on arbitrary input is unsafe as it could crash
156155
-- | Wrap it so it doesn't crash, TODO: fix in hevm
157156
safeMakeSrcMaps :: T.Text -> IO (Maybe (Seq SrcMap))
158157
safeMakeSrcMaps x =
159158
-- $! forces the exception to happen right here so we can catch it
160159
catch (pure $! makeSrcMaps x) (\(_ :: SomeException) -> pure Nothing)
160+
161+
-- | Unified interface for fetching contract source data from Etherscan
162+
-- Returns Nothing if no API key is provided
163+
fetchContractSourceData
164+
:: Maybe W256 -- ^ chainId (optional, defaults to mainnet)
165+
-> Maybe Text -- ^ Etherscan API key (returns Nothing if not provided)
166+
-> String -- ^ Block explorer URL (for HTML scraping)
167+
-> Addr -- ^ contract address
168+
-> IO (Maybe SourceData)
169+
fetchContractSourceData _ Nothing _ _ = pure Nothing
170+
fetchContractSourceData maybeChainId maybeApiKey explorerUrl addr = do
171+
srcRet <- fetchContractSource maybeChainId maybeApiKey addr
172+
srcmapRet <- fetchContractSourceMap explorerUrl addr
173+
pure $ do
174+
src <- srcRet
175+
Just $ SourceData
176+
{ sourceFiles = Map.singleton (src.name <> ".sol") (T.pack src.code)
177+
, runtimeSrcMap = srcmapRet
178+
, creationSrcMap = Nothing
179+
, contractName = src.name
180+
, abi = Nothing
181+
, immutableRefs = Nothing
182+
}

0 commit comments

Comments
 (0)