Skip to content

Commit b66daf2

Browse files
authored
Initial commit
0 parents  commit b66daf2

File tree

9 files changed

+387
-0
lines changed

9 files changed

+387
-0
lines changed

.github/workflows/ci.yml

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
name: CI
2+
3+
on:
4+
workflow_dispatch:
5+
pull_request:
6+
push:
7+
branches:
8+
- main
9+
10+
env:
11+
FOUNDRY_PROFILE: ci
12+
13+
jobs:
14+
build:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v3
18+
19+
- name: Install Foundry
20+
uses: foundry-rs/foundry-toolchain@v1
21+
22+
- name: Build contracts
23+
run: |
24+
forge --version
25+
forge build --sizes
26+
27+
test:
28+
runs-on: ubuntu-latest
29+
steps:
30+
- uses: actions/checkout@v3
31+
32+
- name: Install Foundry
33+
uses: foundry-rs/foundry-toolchain@v1
34+
35+
- name: Run tests
36+
run: forge test
37+
38+
coverage:
39+
runs-on: ubuntu-latest
40+
steps:
41+
- uses: actions/checkout@v3
42+
43+
- name: Install Foundry
44+
uses: foundry-rs/foundry-toolchain@v1
45+
46+
- name: Run coverage
47+
run: forge coverage --report summary --report lcov
48+
49+
# To ignore coverage for certain directories modify the paths in this step as needed. The
50+
# below default ignores coverage results for the test and script directories. Alternatively,
51+
# to include coverage in all directories, comment out this step. Note that because this
52+
# filtering applies to the lcov file, the summary table generated in the previous step will
53+
# still include all files and directories.
54+
# The `--rc lcov_branch_coverage=1` part keeps branch info in the filtered report, since lcov
55+
# defaults to removing branch info.
56+
- name: Filter directories
57+
run: |
58+
sudo apt update && sudo apt install -y lcov
59+
lcov --remove lcov.info 'test/*' 'script/*' --output-file lcov.info --rc lcov_branch_coverage=1
60+
61+
# This step posts a detailed coverage report as a comment and deletes previous comments on
62+
# each push. The below step is used to fail coverage if the specified coverage threshold is
63+
# not met. The below step can post a comment (when it's `github-token` is specified) but it's
64+
# not as useful, and this action cannot fail CI based on a minimum coverage threshold, which
65+
# is why we use both in this way.
66+
- name: Post coverage report
67+
if: github.event_name == 'pull_request' # This action fails when ran outside of a pull request.
68+
uses: romeovs/[email protected]
69+
with:
70+
delete-old-comments: true
71+
lcov-file: ./lcov.info
72+
github-token: ${{ secrets.GITHUB_TOKEN }} # Adds a coverage summary comment to the PR.
73+
74+
- name: Verify minimum coverage
75+
uses: zgosalvez/github-actions-report-lcov@v2
76+
with:
77+
coverage-files: ./lcov.info
78+
minimum-coverage: 100 # Set coverage threshold.
79+
80+
lint:
81+
runs-on: ubuntu-latest
82+
steps:
83+
- uses: actions/checkout@v3
84+
85+
- name: Install Foundry
86+
uses: foundry-rs/foundry-toolchain@v1
87+
88+
- name: Install scopelint
89+
uses: engineerd/[email protected]
90+
with:
91+
name: scopelint
92+
repo: ScopeLift/scopelint
93+
fromGitHubReleases: true
94+
version: latest
95+
pathInArchive: scopelint-x86_64-linux/scopelint
96+
urlTemplate: https://github.com/ScopeLift/scopelint/releases/download/{{version}}/scopelint-x86_64-linux.tar.xz
97+
token: ${{ secrets.GITHUB_TOKEN }}
98+
99+
- name: Check formatting
100+
run: |
101+
scopelint --version
102+
scopelint check
103+
104+
slither-analyze:
105+
runs-on: ubuntu-latest
106+
permissions:
107+
security-events: write
108+
steps:
109+
- uses: actions/checkout@v3
110+
111+
- name: Run Slither
112+
uses: crytic/[email protected]
113+
id: slither # Required to reference this step in the next step.
114+
with:
115+
fail-on: none # Required to avoid failing the CI run regardless of findings.
116+
sarif: results.sarif
117+
slither-args: --filter-paths "./lib|./test" --exclude naming-convention
118+
119+
- name: Upload SARIF file
120+
uses: github/codeql-action/upload-sarif@v2
121+
with:
122+
sarif_file: ${{ steps.slither.outputs.sarif }}

.gitignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Compiler files
2+
cache/
3+
out/
4+
5+
# Ignores development broadcast logs
6+
!/broadcast
7+
/broadcast/*/31337/
8+
/broadcast/**/dry-run/
9+
10+
# Dotenv file
11+
.env
12+
13+
# Coverage
14+
lcov.info

.gitmodules

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

README.md

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# ScopeLift Foundry Template
2+
3+
An opinionated template for [Foundry](https://github.com/foundry-rs/foundry) projects.
4+
5+
_**Please read the full README before using this template.**_
6+
7+
- [Usage](#usage)
8+
- [Overview](#overview)
9+
- [`foundry.toml`](#foundrytoml)
10+
- [CI](#ci)
11+
- [Test Structure](#test-structure)
12+
- [Configuration](#configuration)
13+
- [Coverage](#coverage)
14+
- [Slither](#slither)
15+
- [GitHub Code Scanning](#github-code-scanning)
16+
17+
## Usage
18+
19+
To use this template, use one of the below approaches:
20+
21+
1. Run `forge init --template ScopeLift/foundry-template` in an empty directory.
22+
2. Click [here](https://github.com/ScopeLift/foundry-template/generate) to generate a new repository from this template.
23+
3. Click the "Use this template" button from this repo's [home page](https://github.com/ScopeLift/foundry-template).
24+
25+
It's also recommend to install [scopelint](https://github.com/ScopeLift/scopelint), which is used in CI.
26+
You can run this locally with `scopelint fmt` and `scopelint check`.
27+
Note that these are supersets of `forge fmt` and `forge fmt --check`, so you do not need to run those forge commands when using scopelint.
28+
29+
## Overview
30+
31+
This template is designed to be a simple but powerful configuration for Foundry projects, that aims to help you follow Solidity and Foundry [best practices](https://book.getfoundry.sh/tutorials/best-practices)
32+
Writing secure contracts is hard, so it ships with strict defaults that you can loosen as needed.
33+
34+
### `foundry.toml`
35+
36+
The `foundry.toml` config file comes with:
37+
38+
- A `fmt` configuration.
39+
- `default`, `lite`, and `ci` profiles.
40+
41+
Both of these can of course be modified.
42+
The `default` and `ci` profiles use the same solc build settings, which are intended to be the production settings, but the `ci` profile is configured to run deeper fuzz and invariant tests.
43+
The `lite` profile turns the optimizer off, which is useful for speeding up compilation times during development.
44+
45+
It's recommended to keep the solidity configuration of the `default` and `ci` profiles in sync, to avoid accidentally deploying contracts with suboptimal configuration settings when running `forge script`.
46+
This means you can change the solc settings in the `default` profile and the `lite` profile, but never for the `ci` profile.
47+
48+
Note that the `foundry.toml` file is formatted using [Taplo](https://taplo.tamasfe.dev/) via `scopelint fmt`.
49+
50+
### CI
51+
52+
Robust CI is also included, with a GitHub Actions workflow that does the following:
53+
54+
- Runs tests with the `ci` profile.
55+
- Verifies contracts are within the [size limit](https://eips.ethereum.org/EIPS/eip-170) of 24576 bytes.
56+
- Runs `forge coverage` and verifies a minimum coverage threshold is met.
57+
- Runs `slither`, integrated with GitHub's [code scanning](https://docs.github.com/en/code-security/code-scanning). See the [Configuration](#configuration) section to learn more.
58+
59+
The CI also runs [scopelint](https://github.com/ScopeLift/scopelint) to verify formatting and best practices:
60+
61+
- Checks that Solidity and TOML files have been formatted.
62+
- Solidity checks use the `foundry.toml` config.
63+
- Currently the TOML formatting cannot be customized.
64+
- Validates test names follow a convention of `test(Fork)?(Fuzz)?_(Revert(If_|When_){1})?\w{1,}`. [^naming-convention]
65+
- Validates constants and immutables are in `ALL_CAPS`.
66+
- Validates internal functions in `src/` start with a leading underscore.
67+
- Validates function names and visibility in forge scripts to 1 public `run` method per script. [^script-abi]
68+
69+
Note that the foundry-toolchain GitHub Action will cache RPC responses in CI by default, and it will also update the cache when you update your fork tests.
70+
71+
### Test Structure
72+
73+
The test structure is configured to follow recommended [best practices](https://book.getfoundry.sh/tutorials/best-practices).
74+
It's strongly recommended to read that document, as it covers a range of aspects.
75+
Consequently, the test structure is as follows:
76+
77+
- The core protocol deploy script is `script/Deploy.sol`.
78+
This deploys the contracts and saves their addresses to storage variables.
79+
- The tests inherit from this deploy script and execute `Deploy.run()` in their `setUp` method.
80+
This has the effect of running all tests against your deploy script, giving confidence that your deploy script is correct.
81+
- Each test contract serves as `describe` block to unit test a function, e.g. `contract Increment` to test the `increment` function.
82+
83+
## Configuration
84+
85+
After creating a new repository from this template, make sure to set any desired [branch protections](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/about-protected-branches) on your repo.
86+
87+
### Coverage
88+
89+
The [`ci.yml`](.github/workflows/ci.yml) has `coverage` configured by default, and contains comments explaining how to modify the configuration.
90+
It uses:
91+
The [lcov] CLI tool to filter out the `test/` and `script/` folders from the coverage report.
92+
93+
- The [romeovs/lcov-reporter-action](https://github.com/romeovs/lcov-reporter-action) action to post a detailed coverage report to the PR. Subsequent commits on the same branch will automatically delete stale coverage comments and post new ones.
94+
- The [zgosalvez/github-actions-report-lcov](https://github.com/zgosalvez/github-actions-report-lcov) action to fail coverage if a minimum coverage threshold is not met.
95+
96+
Be aware of foundry's current coverage limitations:
97+
98+
- You cannot filter files/folders from `forge` directly, so `lcov` is used to do this.
99+
- `forge coverage` always runs with the optimizer off and without via-ir, so if you need either of these to compile you will not be able to run coverage.
100+
101+
Remember not to optimize for coverage, but to optimize for [well thought-out tests](https://book.getfoundry.sh/tutorials/best-practices?highlight=coverage#best-practices-1).
102+
103+
### Slither
104+
105+
In [`ci.yml`](.github/workflows/ci.yml), you'll notice Slither is configured as follows:
106+
107+
```yml
108+
slither-args: --filter-paths "./lib|./test" --exclude naming-convention
109+
```
110+
111+
This means Slither is not run on the `lib` or `test` folders, and the [`naming-convention`](https://github.com/crytic/slither/wiki/Detector-Documentation#conformance-to-solidity-naming-conventions) check is disabled.
112+
This `slither-args` field is where you can change the Slither configuration for your project.
113+
114+
The [`solc-version`](https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-versions-of-solidity) check is another check you may want to disable.
115+
The `--exclude` flag takes a comma-separated list, so you can do this like so:
116+
117+
```yml
118+
slither-args: --filter-paths "./lib|./test" --exclude naming-convention,solc-version
119+
```
120+
121+
Notice that Slither will run against `script/` by default.
122+
Carefully written and tested scripts are key to ensuring complex deployment and scripting pipelines execute as planned, but you are free to disable Slither checks on the scripts folder if it feels like overkill for your use case.
123+
124+
For more information on configuration Slither, see [the documentation](https://github.com/crytic/slither/wiki/Usage). For more information on configuring the slither action, see the [slither-action](https://github.com/crytic/slither-action) repo.
125+
126+
### GitHub Code Scanning
127+
128+
As mentioned, the Slither CI step is integrated with GitHub's [code scanning](https://docs.github.com/en/code-security/code-scanning) feature.
129+
This means when your jobs execute, you'll see two related checks:
130+
131+
1. `CI / slither-analyze`
132+
2. `Code scanning results / Slither`
133+
134+
The first check is the actual Slither analysis.
135+
You'll notice in the [`ci.yml`](.github/workflows/ci.yml) file that this check has a configuration of `fail-on: none`.
136+
This means this step will _never_ fail CI, no matter how many findings there are or what their severity is.
137+
Instead, this check outputs the findings to a SARIF file[^sarif] to be used in the next check.
138+
139+
The second check is the GitHub code scanning check.
140+
The `slither-analyze` job uploads the SARIF report to GitHub, which is then analyzed by GitHub's code scanning feature in this step.
141+
This is the check that will fail CI if there are Slither findings.
142+
143+
By default when you create a repository, only alerts with the severity level of `Error` will cause a pull request check failure, and checks will succeed with alerts of lower severities.
144+
However, you can [configure](https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#defining-the-severities-causing-pull-request-check-failure) which level of slither results cause PR check failures.
145+
146+
It's recommended to conservatively set the failure level to `Any` to start, and to reduce the failure level if you are unable to sufficiently tune Slither or find it to be too noisy.
147+
148+
Findings are shown directly on the PR, as well as in your repo's "Security" tab, under the "Code scanning" section.
149+
Alerts that are dismissed are remembered by GitHub, and will not be shown again on future PRs.
150+
151+
Note that code scanning integration [only works](https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/setting-up-code-scanning-for-a-repository) for public repos, or private repos with GitHub Enterprise Cloud and a license for GitHub Advanced Security.
152+
If you have a private repo and don't want to purchase a license, the best option is probably to:
153+
154+
- Remove the `Upload SARIF file` step from CI.
155+
- Change the `Run Slither` step to `fail-on` whichever level you like, and remove the `sarif` output.
156+
- Use [triage mode](https://github.com/crytic/slither/wiki/Usage#triage-mode) locally and commit the resulting `slither.db.json` file, and make sure CI has access to that file.
157+
158+
[^naming-convention]:
159+
A rigorous test naming convention is important for ensuring that tests are easy to understand and maintain, while also making filtering much easier.
160+
For example, one benefit is filtering out all reverting tests when generating gas reports.
161+
162+
[^script-abi]: Limiting scripts to a single public method makes it easier to understand a script's purpose, and facilitates composability of simple, atomic scripts.
163+
[^sarif]:
164+
[SARIF](https://sarifweb.azurewebsites.net/) (Static Analysis Results Interchange Format) is an industry standard for static analysis results.
165+
You can read learn more about SARIF [here](https://github.com/microsoft/sarif-tutorials) and read about GitHub's SARIF support [here](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning).

foundry.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[profile.default]
2+
optimizer = true
3+
optimizer_runs = 10_000_000
4+
solc_version = "0.8.16"
5+
verbosity = 3
6+
7+
[profile.ci]
8+
fuzz = { runs = 5000 }
9+
invariant = { runs = 1000 }
10+
11+
[profile.lite]
12+
fuzz = { runs = 50 }
13+
invariant = { runs = 10 }
14+
# Speed up compilation and tests during development.
15+
optimizer = false
16+
17+
[fmt]
18+
bracket_spacing = false
19+
int_types = "long"
20+
line_length = 100
21+
multiline_func_header = "attributes_first"
22+
number_underscore = "thousands"
23+
quote_style = "double"
24+
single_line_statement_blocks = "single"
25+
tab_width = 2
26+
wrap_comments = true

lib/forge-std

Submodule forge-std added at 76e89e5

script/Deploy.s.sol

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
3+
pragma solidity ^0.8.16;
4+
5+
import "forge-std/Script.sol";
6+
import {Counter} from "src/Counter.sol";
7+
8+
contract Deploy is Script {
9+
Counter counter;
10+
11+
function run() public {
12+
// Commented out for now until https://github.com/crytic/slither/pull/1461 is released.
13+
// vm.startBroadcast();
14+
counter = new Counter();
15+
}
16+
}

src/Counter.sol

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.16;
3+
4+
contract Counter {
5+
uint256 public number;
6+
7+
function setNumber(uint256 newNumber) public {
8+
number = newNumber;
9+
}
10+
11+
function increment() public {
12+
number++;
13+
}
14+
}

test/Counter.t.sol

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.16;
3+
4+
import "forge-std/Test.sol";
5+
import {Deploy} from "script/Deploy.s.sol";
6+
import {Counter} from "src/Counter.sol";
7+
8+
contract CounterTest is Test, Deploy {
9+
function setUp() public {
10+
Deploy.run();
11+
}
12+
}
13+
14+
contract Increment is CounterTest {
15+
function test_NumberIsIncremented() public {
16+
counter.increment();
17+
assertEq(counter.number(), 1);
18+
}
19+
}
20+
21+
contract SetNumber is CounterTest {
22+
function testFuzz_NumberIsSet(uint256 x) public {
23+
counter.setNumber(x);
24+
assertEq(counter.number(), x);
25+
}
26+
}

0 commit comments

Comments
 (0)