Skip to content

Commit 79896a0

Browse files
authored
Merge branch 'master' into fixed-sym-exec
2 parents cccc128 + e871c88 commit 79896a0

File tree

21 files changed

+155
-44
lines changed

21 files changed

+155
-44
lines changed

CHANGELOG.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1-
## Unreleased
1+
## 2.2.7
22

33
* feat: show which project is being fuzzed (#1381)
4+
* feat: keyboard navigation for the UI (Tab, PgUp, PgDown, arrows) (#1386)
5+
* feat: gas/s reporting on text mode (#1392)
46
* ARM64 Docker containers (#1352)
7+
* ARM64 Linux builds (#1377)
58
* Fix worker crashes when shrinking empty reproducers (#1378)
6-
* Upgrade `hevm` to reduce memory usage on certain scenarios (#1346)
9+
* Fix shrinking sometimes not progressing (#1399)
10+
* Fix gas accounting; it was not considering the intrinsic cost of transactions (#1392)
11+
* Fix issue collecting deployed contract addresses into the dictionary (#1400)
12+
* Improved UI responsiveness (#1387)
13+
* Update `hevm` to reduce memory usage on certain scenarios (#1346)
14+
* Update `hevm` to fix multiple deployments under `prank`ing cheatcodes (#1377)
15+
* Echidna is now built with GHC 9.8.4 (#1377)
716

817
## 2.2.6
918

flake.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
(pkgs.haskellPackages.callCabal2nix "hevm" (pkgs.fetchFromGitHub {
5656
owner = "ethereum";
5757
repo = "hevm";
58-
rev = "2931f09fcbbca68911421fbe2f2f21ebebdb5332";
58+
rev = "release/0.55.0";
5959
sha256 = "sha256-DVwQisTwgSjZQkQSn5s7ZN3SS1z2EfISA6Lw0ruUg6Y=";
6060
}) { secp256k1 = pkgs.secp256k1; })
6161
([

lib/Echidna/ABI.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ mkValidAbiUInt i x = if x < bit i then Just $ AbiUInt i x else Nothing
5555

5656
makeNumAbiValues :: Integer -> [AbiValue]
5757
makeNumAbiValues i =
58-
let l f = f <$> commonTypeSizes <*> fmap fromIntegral ([i-1..i+1] ++ [(-i)-1 .. (-i)+1])
58+
let l f = f <$> commonTypeSizes <*> fmap fromIntegral ([i-3..i+3] ++ [(-i)-3 .. (-i)+3])
5959
in catMaybes (l mkValidAbiInt ++ l mkValidAbiUInt)
6060

6161
makeArrayAbiValues :: ByteString -> [AbiValue]
@@ -218,7 +218,7 @@ fixAbiInt n x =
218218
else AbiInt n (x `mod` (2 ^ (n - 1) - 1))
219219

220220
-- | Given a way to generate random 'Word8's and a 'ByteString' b of length l,
221-
-- generate between 0 and 2l 'Word8's and add insert them into b at random indices.
221+
-- generate between 0 and 2l 'Word8's and insert them into b at random indices.
222222
addChars :: MonadRandom m => m Word8 -> ByteString -> m ByteString
223223
addChars c b = foldM withR b . enumFromTo 0 =<< rand where
224224
rand = getRandomR (0, BS.length b - 1)

lib/Echidna/Campaign.hs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,7 @@ callseq vm txSeq = do
543543
_ -> Nothing
544544
_ -> Nothing
545545

546-
-- | Add transactions to the corpus discarding reverted ones
546+
-- | Add transactions to the corpus, discarding reverted ones
547547
addToCorpus :: Int -> [(Tx, (VMResult Concrete RealWorld, Gas))] -> Corpus -> Corpus
548548
addToCorpus n res corpus =
549549
if null rtxs then corpus else Set.insert (n, rtxs) corpus
@@ -567,7 +567,7 @@ execTxOptC vm tx = do
567567
pure (res, vm')
568568

569569
-- | Given current `gasInfo` and a sequence of executed transactions, updates
570-
-- information on highest gas usage for each call
570+
-- information on the highest gas usage for each call
571571
updateGasInfo
572572
:: [(Tx, (VMResult Concrete RealWorld, Gas))]
573573
-> [Tx]
@@ -607,8 +607,8 @@ evalSeq vm0 execFunc = go vm0 [] where
607607
-- NOTE: we don't use the intermediate VMs, just the last one. If any of
608608
-- the intermediate VMs are needed, they can be put next to the result
609609
-- of each transaction - `m ([(Tx, result, VM)])`
610-
(remaining, _vm) <- go vm' (tx:executedSoFar) remainingTxs
611-
pure ((tx, result) : remaining, vm')
610+
(remaining, vm'') <- go vm' (tx:executedSoFar) remainingTxs
611+
pure ((tx, result) : remaining, vm'')
612612

613613
-- | Update tests based on the return value from the given function.
614614
-- Nothing skips the update.

lib/Echidna/Shrink.hs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ shrinkTest
3030
shrinkTest vm test = do
3131
env <- ask
3232
case test.state of
33-
-- If we run out of tries to shrink, return the sequence as we have them
33+
-- If we run out of tries to shrink, return the sequence as we have it
3434
Large i | i >= env.cfg.campaignConf.shrinkLimit && not (isOptimizationTest test) ->
3535
pure $ Just test { state = Solved }
3636
Large i ->
@@ -116,7 +116,7 @@ shrinkSeq vm f v txs = do
116116
-- | Simplify a sequence of transactions reducing the complexity of its arguments (using shrinkTx)
117117
-- and then reducing its sender (using shrinkSender)
118118
shrunk = mapM (shrinkSender <=< shrinkTx) txs
119-
-- | Simplifiy a sequence of transactions randomly dropping one transaction (with uniform selection)
119+
-- | Simplify a sequence of transactions randomly dropping one transaction (with uniform selection)
120120
shorten = (\i -> take i txs ++ drop (i + 1) txs) <$> getRandomR (0, length txs)
121121

122122
-- | Given a transaction, replace the sender of the transaction by another one
@@ -128,6 +128,6 @@ shrinkSender x = do
128128
let orderedSenders = List.sort $ Set.toList senderSet
129129
case List.elemIndex x.src orderedSenders of
130130
Just i | i > 0 -> do
131-
sender <- uniform (take i orderedSenders)
131+
sender <- uniform (take (i+1) orderedSenders)
132132
pure x{src = sender}
133133
_ -> pure x

lib/Echidna/Solidity.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ compileContracts solConf fp = do
9797
-- | OS-specific path to the "null" file, which accepts writes without storing them
9898
nullFilePath :: String
9999
nullFilePath = if os == "mingw32" then "\\\\.\\NUL" else "/dev/null"
100-
-- clean up previous artifacts
100+
-- clean up previous artifact files
101101
removeJsonFiles "crytic-export"
102102
mconcat . NE.toList <$> mapM compileOne fp
103103

lib/Echidna/Transaction.hs

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,29 @@ module Echidna.Transaction where
66
import Optics.Core
77
import Optics.State.Operators
88

9-
import Control.Monad (join)
9+
import Control.Monad (join, when)
1010
import Control.Monad.IO.Class (MonadIO, liftIO)
1111
import Control.Monad.Random.Strict (MonadRandom, getRandomR, uniform)
1212
import Control.Monad.Reader (MonadReader, ask)
1313
import Control.Monad.State.Strict (MonadState, gets, modify', execState)
1414
import Control.Monad.ST (RealWorld)
15+
import Data.ByteString qualified as BS
1516
import Data.Map (Map, toList)
1617
import Data.Maybe (catMaybes)
1718
import Data.Set (Set)
1819
import Data.Set qualified as Set
1920
import Data.Vector qualified as V
2021

21-
import EVM (initialContract, loadContract, resetState)
22+
import EVM (ceilDiv, initialContract, loadContract, resetState)
2223
import EVM.ABI (abiValueType)
23-
import EVM.Types hiding (Env, VMOpts(timestamp, gasprice))
24+
import EVM.FeeSchedule (FeeSchedule(..))
25+
import EVM.Types hiding (Env, Gas, VMOpts(timestamp, gasprice))
2426

2527
import Echidna.ABI
2628
import Echidna.Orphans.JSON ()
2729
import Echidna.SourceMapping (lookupUsingCodehash)
2830
import Echidna.SymExec.Symbolic (forceWord, forceAddr)
29-
import Echidna.Types (fromEVM)
31+
import Echidna.Types (fromEVM, Gas)
3032
import Echidna.Types.Config (Env(..), EConfig(..))
3133
import Echidna.Types.Random
3234
import Echidna.Types.Signature
@@ -177,25 +179,49 @@ setupTx tx@Tx{call} = fromEVM $ do
177179
, block = advanceBlock vm.block tx.delay
178180
, tx = vm.tx { gasprice = tx.gasprice, origin = LitAddr tx.src }
179181
}
180-
case call of
181-
SolCreate bc -> do
182-
#env % #contracts % at (LitAddr tx.dst) .=
183-
Just (initialContract (InitCode bc mempty) & set #balance (Lit tx.value))
184-
modify' $ execState $ loadContract (LitAddr tx.dst)
185-
#state % #code .= RuntimeCode (ConcreteRuntimeCode bc)
186-
SolCall cd -> do
187-
incrementBalance
188-
modify' $ execState $ loadContract (LitAddr tx.dst)
189-
#state % #calldata .= ConcreteBuf (encode cd)
190-
SolCalldata cd -> do
191-
incrementBalance
192-
modify' $ execState $ loadContract (LitAddr tx.dst)
193-
#state % #calldata .= ConcreteBuf cd
182+
when isCreate $ do
183+
#env % #contracts % at (LitAddr tx.dst) .=
184+
Just (initialContract (InitCode calldata mempty) & set #balance (Lit tx.value))
185+
modify' $ execState $ loadContract (LitAddr tx.dst)
186+
#state % #code .= RuntimeCode (ConcreteRuntimeCode calldata)
187+
when isCall $ do
188+
incrementBalance
189+
modify' $ execState $ loadContract (LitAddr tx.dst)
190+
#state % #calldata .= ConcreteBuf calldata
191+
modify' $ \vm ->
192+
let intrinsicGas = txGasCost vm.block.schedule isCreate calldata
193+
burned = min intrinsicGas vm.state.gas
194+
in vm & #state % #gas %!~ subtract burned
195+
& #burned %!~ (+ burned)
194196
where
195197
incrementBalance = #env % #contracts % ix (LitAddr tx.dst) % #balance %= (\v -> Lit $ forceWord v + tx.value)
196198
encode (n, vs) = abiCalldata (encodeSig (n, abiValueType <$> vs)) $ V.fromList vs
199+
isCall = case call of
200+
SolCall _ -> True
201+
SolCalldata _ -> True
202+
_ -> False
203+
isCreate = case call of
204+
SolCreate _ -> True
205+
_ -> False
206+
calldata = case call of
207+
SolCreate bc -> bc
208+
SolCall cd -> encode cd
209+
SolCalldata cd -> cd
197210

198211
advanceBlock :: Block -> (W256, W256) -> Block
199212
advanceBlock blk (t,b) =
200213
blk { timestamp = Lit (forceWord blk.timestamp + t)
201-
, number = Lit $ forceWord blk.number + b }
214+
, number = Lit (forceWord blk.number + b) }
215+
216+
-- | Calculate transaction gas cost for Echidna Tx
217+
-- Adapted from HEVM's txGasCost function
218+
txGasCost :: FeeSchedule Gas -> Bool -> BS.ByteString -> Gas
219+
txGasCost fs isCreate calldata = baseCost + zeroCost + nonZeroCost
220+
where
221+
zeroBytes = BS.count 0 calldata
222+
nonZeroBytes = BS.length calldata - zeroBytes
223+
baseCost = fs.g_transaction
224+
+ (if isCreate then fs.g_txcreate + initcodeCost else 0)
225+
zeroCost = fs.g_txdatazero * fromIntegral zeroBytes
226+
nonZeroCost = fs.g_txdatanonzero * fromIntegral nonZeroBytes
227+
initcodeCost = fs.g_initcodeword * fromIntegral (ceilDiv (BS.length calldata) 32)

lib/Echidna/Types/Campaign.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ data CampaignConf = CampaignConf
3636
, corpusDir :: Maybe FilePath
3737
-- ^ Directory to load and save lists of transactions
3838
, mutConsts :: MutationConsts Integer
39-
-- ^ Directory to load and save lists of transactions
39+
-- ^ Mutation constants for fuzzing
4040
, coverageFormats :: [CoverageFileType]
4141
-- ^ List of file formats to save coverage reports
4242
, workers :: Maybe Word8

lib/Echidna/Types/Solidity.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ instance Show SolException where
5151
NoCryticCompile -> "crytic-compile not installed or not found in PATH. To install it, run:\n pip install crytic-compile"
5252
InvalidMethodFilters f -> "Applying the filter " ++ show f ++ " to the methods produces an empty list. Are you filtering the correct functions using `filterFunctions` or fuzzing the correct contract?"
5353
SetUpCallFailed -> "Calling the setUp() function failed (revert, out-of-gas, sending ether to a non-payable constructor, etc.)"
54-
DeploymentFailed a t -> "Deploying the contract " ++ show a ++ " failed (revert, out-of-gas, sending ether to an non-payable constructor, etc.):\n" ++ unpack t
54+
DeploymentFailed a t -> "Deploying the contract " ++ show a ++ " failed (revert, out-of-gas, sending ether to a non-payable constructor, etc.):\n" ++ unpack t
5555
OutdatedSolcVersion v -> "Solc version " ++ toString v ++ " detected. Echidna doesn't support versions of solc before " ++ toString minSupportedSolcVersion ++ ". Please use a newer version."
5656

5757

lib/Echidna/UI.hs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ data UIEvent =
5555
(Map Addr (Map W256 (Maybe W256)))
5656
| EventReceived (LocalTime, CampaignEvent)
5757

58+
-- | Gas tracking state for calculating gas consumption rate
59+
data GasTracker = GasTracker
60+
{ lastUpdateTime :: LocalTime
61+
, totalGasConsumed :: Int
62+
}
63+
5864
-- | Set up and run an Echidna 'Campaign' and display interactive UI or
5965
-- print non-interactive output in desired format at the end
6066
ui
@@ -176,10 +182,14 @@ ui vm dict initialCorpus cliSelectedContract = do
176182
let forwardEvent ev = putStrLn =<< runReaderT (ppLogLine vm ev) env
177183
uiEventsForwarderStopVar <- spawnListener forwardEvent
178184

185+
-- Track last update time and gas for delta calculation
186+
startTime <- liftIO getTimestamp
187+
lastUpdateRef <- liftIO $ newIORef $ GasTracker startTime 0
188+
179189
let printStatus = do
180190
states <- liftIO $ workerStates workers
181191
time <- timePrefix <$> getTimestamp
182-
line <- statusLine env states
192+
line <- statusLine env states lastUpdateRef
183193
putStrLn $ time <> "[status] " <> line
184194
hFlush stdout
185195

@@ -196,7 +206,7 @@ ui vm dict initialCorpus cliSelectedContract = do
196206

197207
liftIO $ killThread ticker
198208

199-
-- print final status regardless the last scheduled update
209+
-- print final status regardless of the last scheduled update
200210
liftIO printStatus
201211

202212
when (isJust conf.campaignConf.serverPort) $ do
@@ -295,7 +305,7 @@ monitor = do
295305
state <- get
296306
let updatedState = state { campaigns = c', status = Running, now, tests }
297307
newWidget <- liftIO $ runReaderT (campaignStatus updatedState) env
298-
-- purposedly using lazy modify here, so unnecesary widget states don't get computed
308+
-- intentionally using lazy modify here, so unnecessary widget states don't get computed
299309
modify $ const updatedState { campaignWidget = newWidget }
300310
AppEvent (FetchCacheUpdated contracts slots) ->
301311
modify' $ \state ->
@@ -381,15 +391,27 @@ isTerminal = hNowSupportsANSI stdout
381391
statusLine
382392
:: Env
383393
-> [WorkerState]
394+
-> IORef GasTracker -- Gas consumption tracking state
384395
-> IO String
385-
statusLine env states = do
396+
statusLine env states lastUpdateRef = do
386397
tests <- traverse readIORef env.testRefs
387398
(points, _) <- coverageStats env.coverageRefInit env.coverageRefRuntime
388399
corpus <- readIORef env.corpusRef
400+
now <- getTimestamp
389401
let totalCalls = sum ((.ncalls) <$> states)
402+
let totalGas = sum ((.totalGas) <$> states)
403+
404+
-- Calculate delta-based gas/s
405+
gasTracker <- readIORef lastUpdateRef
406+
let deltaTime = round $ diffLocalTime now gasTracker.lastUpdateTime
407+
let deltaGas = totalGas - gasTracker.totalGasConsumed
408+
let gasPerSecond = if deltaTime > 0 then deltaGas `div` deltaTime else 0
409+
writeIORef lastUpdateRef $ GasTracker now totalGas
410+
390411
pure $ "tests: " <> show (length $ filter didFail tests) <> "/" <> show (length tests)
391412
<> ", fuzzing: " <> show totalCalls <> "/" <> show env.cfg.campaignConf.testLimit
392413
<> ", values: " <> show ((.value) <$> filter isOptimizationTest tests)
393414
<> ", cov: " <> show points
394415
<> ", corpus: " <> show (Corpus.corpusSize corpus)
416+
<> ", gas/s: " <> show gasPerSecond
395417

0 commit comments

Comments
 (0)