Skip to content

Commit 3720fba

Browse files
merge
2 parents 8b0d246 + 4974580 commit 3720fba

37 files changed

Lines changed: 823 additions & 124 deletions

.github/workflows/ci.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ jobs:
188188
run: GZIP=-9 tar -czf echidna.tar.gz -C $APPDATA/local/bin/ echidna.exe
189189

190190
- name: Upload artifact
191-
uses: actions/upload-artifact@v6
191+
uses: actions/upload-artifact@v7
192192
with:
193193
name: echidna-${{ runner.os }}
194194
path: echidna.tar.gz
@@ -202,7 +202,7 @@ jobs:
202202
203203
- name: Upload testsuite
204204
if: runner.os != 'macOS'
205-
uses: actions/upload-artifact@v6
205+
uses: actions/upload-artifact@v7
206206
with:
207207
name: echidna-testsuite-${{ runner.os }}
208208
path: echidna-testsuite*
@@ -232,11 +232,13 @@ jobs:
232232
steps:
233233
- name: Checkout
234234
uses: actions/checkout@v6
235+
with:
236+
submodules: recursive
235237

236238
- name: Setup Python
237239
uses: actions/setup-python@v6
238240
with:
239-
python-version: '3.8'
241+
python-version: '3.10'
240242

241243
- name: Install dependencies
242244
shell: bash
@@ -253,7 +255,7 @@ jobs:
253255
uses: foundry-rs/foundry-toolchain@v1
254256

255257
- name: Download testsuite
256-
uses: actions/download-artifact@v7
258+
uses: actions/download-artifact@v8
257259
with:
258260
name: echidna-testsuite-${{ runner.os }}
259261

.github/workflows/docker.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ jobs:
2727
PLATFORM: "${{ matrix.platform }}"
2828

2929
- name: Set up Docker Buildx
30-
uses: docker/setup-buildx-action@v3
30+
uses: docker/setup-buildx-action@v4
3131
id: buildx
3232
with:
3333
install: true
3434

3535
- name: Set Docker metadata (Ubuntu & NVM variant)
3636
id: meta-ubuntu
37-
uses: docker/metadata-action@v5
37+
uses: docker/metadata-action@v6
3838
with:
3939
images: |
4040
ghcr.io/${{ github.repository }}/echidna
@@ -67,7 +67,7 @@ jobs:
6767
password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }}
6868

6969
- name: Docker Build and Push (Ubuntu & NVM variant)
70-
uses: docker/build-push-action@v6
70+
uses: docker/build-push-action@v7
7171
id: build-ubuntu
7272
with:
7373
platforms: ${{ matrix.platform }}
@@ -87,7 +87,7 @@ jobs:
8787
DIGEST_UBUNTU: "${{ steps.build-ubuntu.outputs.digest }}"
8888

8989
- name: Upload digest
90-
uses: actions/upload-artifact@v6
90+
uses: actions/upload-artifact@v7
9191
with:
9292
name: digests-${{ env.PLATFORM_PAIR }}
9393
path: ${{ runner.temp }}/digests/*
@@ -101,7 +101,7 @@ jobs:
101101
- build
102102
steps:
103103
- name: Download digests
104-
uses: actions/download-artifact@v7
104+
uses: actions/download-artifact@v8
105105
with:
106106
path: ${{ runner.temp }}/digests
107107
pattern: digests-*
@@ -122,11 +122,11 @@ jobs:
122122
password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }}
123123

124124
- name: Set up Docker Buildx
125-
uses: docker/setup-buildx-action@v3
125+
uses: docker/setup-buildx-action@v4
126126

127127
- name: Set Docker metadata (Ubuntu & NVM variant)
128128
id: meta-ubuntu
129-
uses: docker/metadata-action@v5
129+
uses: docker/metadata-action@v6
130130
with:
131131
images: |
132132
ghcr.io/${{ github.repository }}/echidna

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ jobs:
7777
NIX_SYSTEM: ${{ matrix.system }}
7878

7979
- name: Upload artifact
80-
uses: actions/upload-artifact@v6
80+
uses: actions/upload-artifact@v7
8181
with:
8282
name: echidna-redistributable-${{ matrix.tuple }}
8383
path: echidna-${{ steps.version.outputs.version }}-${{ matrix.tuple }}.tar.gz
@@ -96,7 +96,7 @@ jobs:
9696
uses: actions/checkout@v6
9797

9898
- name: Download binaries
99-
uses: actions/download-artifact@v7
99+
uses: actions/download-artifact@v8
100100
with:
101101
pattern: echidna-redistributable-*
102102
merge-multiple: true

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "tests/solidity/foundry/forge-std"]
2+
path = tests/solidity/foundry/forge-std
3+
url = https://github.com/foundry-rs/forge-std

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@ $ echidna tests/solidity/basic/flags.sol
5454

5555
Echidna should find a call sequence that falsifies `echidna_sometimesfalse` and should be unable to find a falsifying input for `echidna_alwaystrue`.
5656

57+
### Testing modes
58+
59+
The example above uses the default **property** mode, but Echidna supports several testing modes, configured via `testMode` in the config file or `--test-mode` on the CLI:
60+
61+
* **`property`** (default): Test `echidna_`-prefixed functions that return `bool`.
62+
* **`assertion`**: Detect assertion failures from `assert()` and Foundry's `assertX` helpers (`assertTrue`, `assertEq`, etc.).
63+
* **`foundry`**: Run Foundry-style `test`-prefixed unit tests and `invariant_`-prefixed stateful invariants.
64+
* **`overflow`**: Detect integer over/underflows (Solidity >= 0.8.0).
65+
* **`optimization`**: Maximize the return value of `echidna_`-prefixed functions that return `int256` (uses the same configurable prefix as property mode).
66+
* **`exploration`**: Collect coverage without checking properties.
67+
5768
### Collecting and visualizing coverage
5869

5970
After finishing a campaign, Echidna can save a coverage maximizing **corpus** in a special directory specified with the `corpusDir` config option. This directory will contain two entries: (1) a directory named `coverage` with JSON files that can be replayed by Echidna and (2) a plain-text file named `covered.txt`, a copy of the source code with coverage annotations.
@@ -143,8 +154,8 @@ Transaction = {
143154

144155
`Coverage` is a dict describing certain coverage-increasing calls. These interfaces are
145156
subject to change to be slightly more user-friendly at a later date. `testType`
146-
will either be `property` or `assertion`, and `status` always takes on either
147-
`fuzzing`, `shrinking`, `solved`, `passed`, or `error`.
157+
will be one of `property`, `assertion`, `optimization`, `exploration`, or `call`,
158+
and `status` always takes on either `fuzzing`, `shrinking`, `solved`, `passed`, or `error`.
148159

149160
### Debugging Performance Problems
150161

flake.nix

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@
6262
(pkgs.haskellPackages.callCabal2nix "hevm" (pkgs.fetchFromGitHub {
6363
owner = "argotorg";
6464
repo = "hevm";
65-
rev = "41e6d1304411749ea8c816d131991663b5dca67a";
66-
sha256 = "sha256-JF4IyQ3OvfrIqybtCvCpz6nw6kgo39LLJSxOjXsw3/c=";
65+
rev = "ed90053fa0ed69e658a75ab0ed64d467f5a5448d";
66+
sha256 = "sha256-drWR25sF1DyPie8oxXvI8N20Ee3YQ9l/7n9VIUg/wXY=";
6767
}) { secp256k1 = pkgs.secp256k1; })
6868
([
6969
pkgs.haskell.lib.compose.dontCheck

lib/Echidna.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,12 @@ prepareContract
5959
-> IO (VM Concrete, Env, GenDict)
6060
prepareContract cfg solFiles buildOutput selectedContract seed = do
6161
let solConf = cfg.solConf
62+
campaignConf = cfg.campaignConf
6263
(Contracts contractMap) = buildOutput.contracts
6364
contracts = Map.elems contractMap
6465

6566
mainContract <- selectMainContract solConf selectedContract contracts
66-
tests <- mkTests solConf mainContract
67+
tests <- mkTests solConf campaignConf mainContract
6768
signatureMap <- mkSignatureMap solConf mainContract contracts
6869

6970
-- run processors

lib/Echidna/Exec.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ classifyError = \case
5656
StackUnderrun -> IllegalE
5757
BadJumpDestination -> IllegalE
5858
IllegalOverflow -> RevertE
59+
AssumeCheatFailed -> RevertE
5960
_ -> UnknownE
6061

6162
-- | Extracts the 'Query' if there is one.

lib/Echidna/Output/Foundry.hs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,10 @@ foundryTx senders tx =
7676
prelude =
7777
(if time > 0 || blocks > 0 then " _delay(" ++ show time ++ ", " ++ show blocks ++ ");\n" else "") ++
7878
" _setUpActor(" ++ senderName ++ ");"
79-
call = " Target." ++ unpack name ++ "(" ++ foundryArgs (map abiValueToString args) ++ ");"
79+
-- Handle fallback function (empty name).
80+
call = if unpack name == ""
81+
then " address(Target).call(\"\");"
82+
else " Target." ++ unpack name ++ "(" ++ foundryArgs (map abiValueToString args) ++ ");"
8083
in Just $ object ["prelude" .= prelude, "call" .= call]
8184
_ -> Nothing
8285

lib/Echidna/Solidity.hs

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module Echidna.Solidity where
22

33
import Control.Monad (when, unless, forM_)
4+
import Control.Monad.IO.Class (liftIO)
45
import Control.Monad.Catch (MonadThrow(..))
56
import Control.Monad.Extra (whenM)
67
import Control.Monad.Reader (ReaderT(runReaderT))
@@ -41,7 +42,7 @@ import Echidna.ABI
4142
import Echidna.Deploy (deployContracts, deployBytecodes)
4243
import Echidna.Exec (execTx, execTxWithCov, initialVM)
4344
import Echidna.SourceAnalysis.Slither
44-
import Echidna.Test (createTests, isAssertionMode, isPropertyMode, isDapptestMode)
45+
import Echidna.Test (createTests, isAssertionMode, isPropertyMode, isFoundryMode)
4546
import Echidna.Types.Campaign (CampaignConf(..))
4647
import Echidna.Types.Config (EConfig(..), Env(..))
4748
import Echidna.Types.Signature
@@ -146,11 +147,19 @@ filterMethods contractName (Whitelist ic) ms =
146147
filterMethods contractName (Blacklist ig) ms =
147148
NE.filter (\s -> encodeSigWithName contractName s `notElem` ig) ms
148149

149-
-- | Filter methods with arguments, used for dapptest mode
150+
-- | Filter methods for foundry mode. Per Foundry conventions:
151+
-- - Functions prefixed with "test" are test functions (unit or fuzz).
152+
-- Fuzz tests are distinguished by having at least one parameter.
153+
-- See: https://book.getfoundry.sh/forge/fuzz-testing
154+
-- - Functions prefixed with "invariant_" are invariant tests, called in
155+
-- randomized sequences to verify properties that must always hold.
156+
-- See: https://book.getfoundry.sh/forge/invariant-testing
157+
-- - Other functions with arguments are kept as callable targets for
158+
-- invariant test campaigns.
150159
filterMethodsWithArgs :: NonEmpty SolSignature -> NonEmpty SolSignature
151160
filterMethodsWithArgs ms =
152-
case NE.filter (\(n, xs) -> T.isPrefixOf "invariant_" n || not (null xs)) ms of
153-
[] -> error "No dapptest tests found"
161+
case NE.filter (\(n, xs) -> T.isPrefixOf "test" n || (T.isPrefixOf "invariant_" n || not (null xs))) ms of
162+
[] -> error "No foundry tests found"
154163
fs -> NE.fromList fs
155164

156165
abiOf :: Text -> SolcContract -> NonEmpty SolSignature
@@ -223,16 +232,22 @@ loadSpecified env mainContract cs = do
223232
solConf.contractAddr
224233
unlimitedGasPerBlock
225234
(0, 0)
226-
vm4 <- if isDapptestMode solConf.testMode && setUpFunction `elem` abi
235+
-- Call setUp() for any contract that has IS_TEST() in its ABI, regardless of
236+
-- test mode. Contracts following the Foundry/dapptools convention (IS_TEST)
237+
-- expect setUp() to run even in assertion or property mode.
238+
when (isFoundryMode solConf.testMode && is_testFunction `notElem` abi) $
239+
liftIO $ putStrLn "Warning: running in Foundry mode but contract does not have IS_TEST(). setUp() will not be called."
240+
vm4 <- if is_testFunction `elem` abi && setUpFunction `elem` abi
227241
then snd <$> transaction
228242
else pure vm3
229243

230244
case vm4.result of
231-
Just (VMFailure _) -> throwM SetUpCallFailed
245+
Just (VMFailure _) -> throwM $ SetUpCallFailed $ showTraceTree env.dapp vm4
232246
_ -> pure vm4
233247

234248
where
235249
setUpFunction = ("setUp", [])
250+
is_testFunction = ("IS_TEST", [])
236251

237252

238253
selectMainContract
@@ -259,7 +274,7 @@ mkSignatureMap
259274
mkSignatureMap solConf mainContract contracts = do
260275
let
261276
-- Filter ABI according to the config options
262-
fabiOfc = if isDapptestMode solConf.testMode
277+
fabiOfc = if isFoundryMode solConf.testMode
263278
then NE.toList $ filterMethodsWithArgs (abiOf solConf.prefix mainContract)
264279
else filterMethods mainContract.contractName solConf.methodFilter $
265280
abiOf solConf.prefix mainContract
@@ -276,22 +291,23 @@ mkSignatureMap solConf mainContract contracts = do
276291
case NE.nonEmpty fabiOfc of
277292
Just ne -> Map.singleton mainContract.runtimeCodehash ne
278293
Nothing -> mempty
279-
when (null abiMapping && isDapptestMode solConf.testMode) $
294+
when (null abiMapping && isFoundryMode solConf.testMode) $
280295
throwM NoTests
281296
when (Map.null abiMapping) $
282297
throwM $ InvalidMethodFilters solConf.methodFilter
283298
pure abiMapping
284299

285300
mkTests
286301
:: SolConf
302+
-> CampaignConf
287303
-> SolcContract
288304
-> IO [EchidnaTest]
289-
mkTests solConf mainContract = do
305+
mkTests solConf campaignConf mainContract = do
290306
let
291307
-- generate the complete abi mapping
292308
abi = Map.elems mainContract.abiMap <&> \method -> (method.name, snd <$> method.inputs)
293309
(tests, funs) = partition (isPrefixOf solConf.prefix . fst) abi
294-
-- Filter again for dapptest tests or assertions checking if enabled
310+
-- Filter again for foundry tests or assertions checking if enabled
295311
neFuns = filterMethods mainContract.contractName
296312
solConf.methodFilter
297313
(fallback NE.:| funs)
@@ -309,6 +325,7 @@ mkTests solConf mainContract = do
309325
pure $ createTests solConf.testMode
310326
solConf.testDestruction
311327
testNames
328+
campaignConf.seqLen
312329
solConf.contractAddr
313330
neFuns
314331

@@ -348,7 +365,7 @@ mkWorld SolConf{sender, testMode} sigMap maybeContract slitherInfo contracts =
348365
payableSigs = filterResults maybeContract slitherInfo.payableFunctions
349366
assertSigs = filterResults maybeContract (assertFunctionList <$> slitherInfo.asserts)
350367
as = if isAssertionMode testMode then filterResults maybeContract (assertFunctionList <$> slitherInfo.asserts) else []
351-
cs = if isDapptestMode testMode then [] else filterResults maybeContract slitherInfo.constantFunctions \\ as
368+
cs = if isFoundryMode testMode then [] else filterResults maybeContract slitherInfo.constantFunctions \\ as
352369
(highSignatureMap, lowSignatureMap) = prepareHashMaps cs as $
353370
filterFallbacks slitherInfo.fallbackDefined slitherInfo.receiveDefined contracts sigMap
354371
in World { senders = sender

0 commit comments

Comments
 (0)