diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a9b5fdbb..7fd53390 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -64,42 +64,6 @@ jobs: - name: Run tests run: yarn test:integration - echidna-tests: - name: Echidna Test - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly - - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: 20.x - cache: 'yarn' - - - name: Install dependencies - run: yarn --frozen-lockfile --network-concurrency 1 - - - name: Compile contracts - run: | - forge build --build-info - - - name: Run Echidna - uses: crytic/echidna-action@v2 - with: - files: . - contract: InvariantGreeter - test-mode: assertion - crytic-args: --ignore-compile - halmos-tests: name: Run symbolic execution tests runs-on: ubuntu-latest diff --git a/README.md b/README.md index 5a4d6546..c35ee249 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@
Sample Integration, Unit, Property-based fuzzed and symbolic tests
Example tests showcasing mocking, assertions and configuration for mainnet forking. As well it includes everything needed in order to check code coverage.
Unit tests are built based on the Branched-Tree Technique, using Bulloak. -
Formal verification and property-based fuzzing are achieved with Halmos and Echidna (resp.). +
Formal verification and property-based fuzzing are achieved with Halmos and Medusa (resp.).
Linter
Simple and fast solidity linting thanks to forge fmt.
@@ -80,7 +80,7 @@ In order to just run integration tests, run: yarn test:integration ``` -In order to just run the echidna fuzzing campaign (requires [Echidna](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/introduction/installation.md) installed), run: +In order to start the Medusa fuzzing campaign (requires [Medusa](https://github.com/crytic/medusa/blob/master/docs/src/getting_started/installation.md) installed), run: ```bash yarn test:fuzz @@ -171,4 +171,4 @@ Also, remember to update the `package_name` param to your package name: You can take a look at our [solidity-exporter-action](https://github.com/defi-wonderland/solidity-exporter-action) repository for more information and usage examples. ## Licensing -The primary license for the boilerplate is MIT, see [`LICENSE`](https://github.com/defi-wonderland/solidity-foundry-boilerplate/blob/main/LICENSE) \ No newline at end of file +The primary license for the boilerplate is MIT, see [`LICENSE`](https://github.com/defi-wonderland/solidity-foundry-boilerplate/blob/main/LICENSE) diff --git a/medusa.json b/medusa.json new file mode 100644 index 00000000..31227c5b --- /dev/null +++ b/medusa.json @@ -0,0 +1,89 @@ +{ + "fuzzing": { + "workers": 10, + "workerResetLimit": 50, + "timeout": 0, + "testLimit": 0, + "shrinkLimit": 5000, + "callSequenceLength": 100, + "corpusDirectory": "", + "coverageEnabled": true, + "coverageFormats": [ + "html", + "lcov" + ], + "targetContracts": ["FuzzTest"], + "predeployedContracts": {}, + "targetContractsBalances": [], + "constructorArgs": {}, + "deployerAddress": "0x30000", + "senderAddresses": [ + "0x10000", + "0x20000", + "0x30000" + ], + "blockNumberDelayMax": 60480, + "blockTimestampDelayMax": 604800, + "blockGasLimit": 125000000, + "transactionGasLimit": 12500000, + "testing": { + "stopOnFailedTest": true, + "stopOnFailedContractMatching": false, + "stopOnNoTests": true, + "testAllContracts": false, + "traceAll": false, + "assertionTesting": { + "enabled": true, + "testViewMethods": true, + "panicCodeConfig": { + "failOnCompilerInsertedPanic": false, + "failOnAssertion": true, + "failOnArithmeticUnderflow": false, + "failOnDivideByZero": false, + "failOnEnumTypeConversionOutOfBounds": false, + "failOnIncorrectStorageAccess": false, + "failOnPopEmptyArray": false, + "failOnOutOfBoundsArrayAccess": false, + "failOnAllocateTooMuchMemory": false, + "failOnCallUninitializedVariable": false + } + }, + "propertyTesting": { + "enabled": false, + "testPrefixes": [ + "property_" + ] + }, + "optimizationTesting": { + "enabled": false, + "testPrefixes": [ + "optimize_" + ] + }, + "targetFunctionSignatures": [], + "excludeFunctionSignatures": [] + }, + "chainConfig": { + "codeSizeCheckDisabled": true, + "cheatCodes": { + "cheatCodesEnabled": true, + "enableFFI": false + }, + "skipAccountChecks": true + } + }, + "compilation": { + "platform": "crytic-compile", + "platformConfig": { + "target": "test/invariants/fuzz/FuzzTest.t.sol", + "solcVersion": "", + "exportDirectory": "", + "args": [] + } + }, + "logging": { + "level": "info", + "logDirectory": "", + "noColor": false + } +} diff --git a/package.json b/package.json index 88242ec3..e2956573 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "lint:sol": "solhint 'src/**/*.sol' 'script/**/*.sol' 'test/**/*.sol'", "prepare": "husky", "test": "forge test -vvv", - "test:fuzz": "echidna test/invariants/fuzz/Greeter.t.sol --contract InvariantGreeter --corpus-dir test/invariants/fuzz/echidna_coverage/ --test-mode assertion", + "test:fuzz": "medusa fuzz", "test:integration": "forge test --match-contract Integration -vvv", "test:symbolic": "halmos", "test:unit": "forge test --match-contract Unit -vvv", diff --git a/test/invariants/PROPERTIES.md b/test/invariants/PROPERTIES.md index e48439c9..bbf31df4 100644 --- a/test/invariants/PROPERTIES.md +++ b/test/invariants/PROPERTIES.md @@ -1,4 +1,4 @@ -| Properties | Type | -|---------------------------------------------------|------------| -| Greeting should never be empty | Valid state | -| Only the owner can set the greeting | State transition | \ No newline at end of file +| Id | Properties | Type | +| --- | --------------------------------------------------- | ------------ | +| 1 | Greeting should never be empty | Valid state | +| 2 | Only the owner can set the greeting | State transition | diff --git a/test/invariants/fuzz/FuzzTest.t.sol b/test/invariants/fuzz/FuzzTest.t.sol new file mode 100644 index 00000000..02a127bf --- /dev/null +++ b/test/invariants/fuzz/FuzzTest.t.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.23; + +import {GreeterGuidedHandlers} from './handlers/guided/Greeter.t.sol'; +import {GreeterUnguidedHandlers} from './handlers/unguided/Greeter.t.sol'; +import {GreeterProperties} from './properties/Greeter.t.sol'; + +contract FuzzTest is GreeterGuidedHandlers, GreeterUnguidedHandlers, GreeterProperties {} diff --git a/test/invariants/fuzz/Greeter.t.sol b/test/invariants/fuzz/Greeter.t.sol deleted file mode 100644 index 96e2b1eb..00000000 --- a/test/invariants/fuzz/Greeter.t.sol +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.23; - -import {Greeter, IERC20} from 'contracts/Greeter.sol'; - -interface IHevm { - function prank(address) external; -} - -contract InvariantGreeter { - IHevm internal _hevm = IHevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); - - Greeter internal _targetContract; - - constructor() { - _targetContract = new Greeter('a', IERC20(address(1))); - } - - function checkGreeterNeverEmpty(string memory _newGreeting) public { - // Execution - (bool _success,) = address(_targetContract).call(abi.encodeCall(Greeter.setGreeting, _newGreeting)); - - // Check output condition - assert((_success && keccak256(bytes(_targetContract.greeting())) != keccak256(bytes(''))) || !_success); - } - - function checkOnlyOwnerSetsGreeting(address _caller) public { - // Input conditions - _hevm.prank(_caller); - - // Execution - (bool _success,) = address(this).call(abi.encodeCall(Greeter.setGreeting, 'hello')); - - // Check output condition - assert((_success && msg.sender == _targetContract.OWNER()) || (!_success && msg.sender != _targetContract.OWNER())); - } -} diff --git a/test/invariants/fuzz/handlers/guided/Greeter.t.sol b/test/invariants/fuzz/handlers/guided/Greeter.t.sol new file mode 100644 index 00000000..732e105e --- /dev/null +++ b/test/invariants/fuzz/handlers/guided/Greeter.t.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.23; + +import {GreeterSetup} from '../../setup/Greeter.t.sol'; + +contract GreeterGuidedHandlers is GreeterSetup { + function handler_setGreeting(string memory _newGreeting) external { + // no need to prank since this contract deployed the greeter and is therefore its owner + try _targetContract.setGreeting(_newGreeting) { + assert(keccak256(bytes(_targetContract.greeting())) == keccak256(bytes(_newGreeting))); + } catch { + assert(keccak256(bytes(_newGreeting)) == keccak256('')); + } + } +} diff --git a/test/invariants/fuzz/handlers/unguided/Greeter.t.sol b/test/invariants/fuzz/handlers/unguided/Greeter.t.sol new file mode 100644 index 00000000..213d73bc --- /dev/null +++ b/test/invariants/fuzz/handlers/unguided/Greeter.t.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.23; + +import {GreeterSetup} from '../../setup/Greeter.t.sol'; + +contract GreeterUnguidedHandlers is GreeterSetup { + /// @custom:property-id 2 + /// @custom:property Only the owner can set the greeting + function handler_setGreeting(address _caller, string memory _newGreeting) external { + vm.prank(_caller); + try _targetContract.setGreeting(_newGreeting) { + assert(keccak256(bytes(_targetContract.greeting())) == keccak256(bytes(_newGreeting))); + assert(_caller == _targetContract.OWNER()); + } catch { + assert(_caller != _targetContract.OWNER() || keccak256(bytes(_newGreeting)) == keccak256('')); + } + } +} diff --git a/test/invariants/fuzz/properties/Greeter.t.sol b/test/invariants/fuzz/properties/Greeter.t.sol new file mode 100644 index 00000000..979d278f --- /dev/null +++ b/test/invariants/fuzz/properties/Greeter.t.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.23; + +import {GreeterSetup} from '../setup/Greeter.t.sol'; + +contract GreeterProperties is GreeterSetup { + /// @custom:property-id 1 + /// @custom:property Greeting should never be empty + function property_greetingIsNeverEmpty() external view { + assert(keccak256(bytes(_targetContract.greeting())) != keccak256('')); + } +} diff --git a/test/invariants/fuzz/setup/Greeter.t.sol b/test/invariants/fuzz/setup/Greeter.t.sol new file mode 100644 index 00000000..ee6676f1 --- /dev/null +++ b/test/invariants/fuzz/setup/Greeter.t.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.23; + +import {Greeter, IERC20} from 'contracts/Greeter.sol'; +import {CommonBase} from 'forge-std/Base.sol'; + +contract GreeterSetup is CommonBase { + Greeter internal _targetContract; + + constructor() { + _targetContract = new Greeter('a', IERC20(address(1))); + } +}