This is a template for Solidity development aimed at the strictest quality compliance settings, with:
- Dedicated Slither & SolHint pre-commit configs for
src/
andtest/
files. - Automatic code coverage badge that is computed by GitHub CI.
- Fuzz testing example with coverage assertion through the invariant testing functionality.
- Code/Branch coverage using LCOV.
- Automatically generated documentation.
To start your own Solidity project, just fork it and start building.
The test/unit/Counter.t.sol
file contains a setUp()
function and 2 unit
tests. They are executed once each, if you run all tests.
Since Fuzz testing uses random values, it can sometimes occur that a certain
test case may not be reached if the random numbers fall unlucky. This could
give a false sense of confidence, because it could also happen if you
incorrectly specify your tests and or random ranges. To ensure all Fuzz
test cases are hit, one can keep a count of how often each test case is hit,
and after all fuzz runs, assert the count is large enough. Unfortunately,
the Foundry Fuzz testing functionality does not (yet) have an afterAll
function for fuzz testing. Fortunately the Invariant test functionality by
Foundry does have an afterAll
function, whilst still allowing random
input data for Fuzz testing.
- A
fuzz-unit test
is a unit test that takes in 1 or more random/unspecified arguments. In the context of this repo, those variables are provided by the Foundry source of randomness provided by the invariant testing functionality.
In this repo, a contract that represents a counter is tested.
- The
Counter.sol
contract contains a number that can be initialised and incremented. That's it. - The
Invariants.sol
manages the Fuzz testing, by initialising the contract that contains thefuzz-unit tests
:setNumber(uint256 x)
andtestUnitIncrement()
which are then called randomly as part of the fuzzing by theinvariantTest()
. After the fuzz testing, asserts that each test case is hit often enough withafterInvariant()
. - The
CounterFuzzUnitTests.sol
contains and performs the actualfuzz-unit tests
that one wants to execute. However, itsfuzz-unit tests
are called by theinvariantTest()
function of theCounterFuzzTestManager
contract.
- The
fuzz-unit tests
are written in a separate file from the manager contract that calls those tests. - Each
fuzz-unit test
file will have a separate manager contract. - The
fuzz-unit tests
contract names are written in format:<Name of contract it tests>FuzzUnitTest
. - The manager contract names are written in format:
<Name of contract it tests>FuzzUnitManager
. - The
fuzz-unit test
function names are written as:testUnit<test description name>
. - An invariant manager/fuzz contract shall contain at least the following 3
functions:
6.1
setUp()
6.2invariantTest()
6.3afterInvariant()
# Install repository configuration.
sudo snap install bun-js
bun install # install Solhint, Prettier, and other Node.js deps
pre-commit install
# Facilitate branch coverage checks.
sudo apt install lcov
# Install foundry
sudo apt install curl -y
curl -L https://foundry.paradigm.xyz | bash
source ~/.bashrc
foundryup
forge build
# Install SolHint (Solidity style guide linterm with autofix.)
sudo apt install npm -y
sudo npm install nodejs
sudo npm install -g solhint
solhint --version
# Install Slither (smart contract static analyzer).
python3 -m pip install slither-analyzer
# Install prettier
npm install --save-dev --save-exact prettier
# Install pre-commit
pre-commit install
git add -A && pre-commit run --all
Build the contracts:
bun install # run this once.
forge build
(If that does not show that the contracts are compiled/does not work, you probably have the wrong forge, a snap package for Ubuntu installed. See solution )
Delete the build artifacts and cache directories:
forge clean
Run the tests:
clear && forge clean && forge test -vvv
Or to run a single test (function):
clear && forge clean && forge test -vvv --match-test testAddTwo
The -vvv
is necessary to display the error messages that you wrote with the
assertions, in the CLI. Otherwise it just says: "test failed".
Get a test coverage report:
clear && forge coverage \
--report lcov --via-ir && genhtml -o report --branch-coverage lcov.info
Get a gas report:
forge test --gas-report
To visualise how the code works you can generate a PlantUML graph of the contracts using:
npm link sol2uml --only=production # Install sol2uml
sol2uml src/ --outputFileName docs/code_diagram.svg
You can create and open the documentation as a website with:
chmod +x create_docs.sh
./create_docs.sh
Or generate the markdown documentation with:forge doc
.
This will create the classDiagram.svg
diagram of the code:
This template comes with GitHub Actions pre-configured. Your contracts will be
linted and tested on every push and pull request made to the main
branch.
You can edit the CI script in .github/workflows/ci.yml.
To ensure the code coverage badge is updated automatically when you push to
main
,you could:
- Go here and create a secret gist named
<your repo name>_branch_coverage.json
- Update the .github/workflows/ci.yml and replace this repository name with
<your repo name>
- Go to your GitHub settings>developer settings>tokens>classic>create a new
personal access token that has the following permissions:
gist
. - Copy that token (secret) and paste it into:
<your repository> > settings > Secrets and variables > Actions
> Repository secrets > New repository secret.
ensure that secret has the name:GIST_SECRET
(and value you just copied). - Then go to the gist by clicking on it in:
https://gist.github.com/<your GitHub username>/
which gives you an url like:https://gist.github.com/a-t-0/59ab053717e0ed834dc2b24304edd5c6
Copy that url and put it in thebranch-coverage-badge-icon
section at the bottom of this Readme file.- Also copy that gist ID into the
.github/workflows/ci.yml
(twice).
- Also copy that gist ID into the
That should be it, now your repo fork has the ability to push the CI results into the gist you just created, and load the badge from that position.
Deploy to Anvil, first open another terminal, give it your custom MNEMONIC
as
an environment variable, and run anvil in it:
# This is a random generated hash with 0 test eth, and the Ethereum test
# network `ethereum-sepolia`
# [faucet](https://www.alchemy.com/faucets/ethereum-sepolia) keeps saying:
# "complete captcha", without showing the captcha (Add block was disabled).
```sh
export MNEMONIC="pepper habit setup conduct material wagon\
captain liquid ill confirm cube easy iron tackle timber"
If you can get the faucet to give you test-ETH, you can use your own MNEMONIC (see BIP39 mnemonic.). Luckily foundry provides a standard test wallet with 1000 ETH in it, which can be used with:
export MNEMONIC="test test test test test test test test test test test junk"
While Anvil runs in the background on another terminal, open a new terminal and run:
forge script script/Deploy.s.sol --broadcast --fork-url http://localhost:8545
By default, this deploys to the HardHat Chain 31337.
For instructions on how to deploy to a testnet or mainnet, check out the Solidity Scripting tutorial.