Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
spengrah authored Jul 4, 2024
0 parents commit b6a68fd
Show file tree
Hide file tree
Showing 12 changed files with 458 additions and 0 deletions.
100 changes: 100 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: ci

on:
push:
branches:
- main
pull_request:

env:
FOUNDRY_PROFILE: ci
INFURA_KEY: ${{ secrets.INFURA_KEY }}
ALCHEMY_KEY: ${{ secrets.ALCHEMY_KEY }}
GC_RPC: ${{ secrets.GC_RPC }}
POLYGON_RPC: ${{ secrets.POLYGON_RPC }}
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}

jobs:

lint:
name: "Markdown linting"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
name: Check out the code
- name: Lint Code Base
uses: docker://avtodev/markdown-lint:v1
with:
args: "**/*.md"
test:
name: "Foundry tests"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

- name: Check formatting
run: forge fmt --check

- name: Check contract sizes
run: forge build --sizes --skip script
# Replace the above line with the below if using pre-compiled ir-optimized contracts
# run: FOUNDRY_PROFILE=optimized forge build --sizes --skip script

- name: Run tests
run: forge test -vvv

coverage:
name: "Test coverage"

env:
FOUNDRY_PROFILE: lite

runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

- name: Run coverage
run: forge coverage --report summary --report lcov

# To ignore coverage for certain directories modify the paths in this step as needed. The
# below default ignores coverage results for the test and script directories. Alternatively,
# to include coverage in all directories, comment out this step. Note that because this
# filtering applies to the lcov file, the summary table generated in the previous step will
# still include all files and directories.
# The `--rc lcov_branch_coverage=1` part keeps branch info in the filtered report, since lcov
# defaults to removing branch info.
- name: Filter directories
run: |
sudo apt update && sudo apt install -y lcov
lcov --remove lcov.info 'src/mocks/*' 'test/*' 'script/*' --output-file lcov.info --rc lcov_branch_coverage=1
# This step posts a detailed coverage report as a comment and deletes previous comments on
# each push. The below step is used to fail coverage if the specified coverage threshold is
# not met. The below step can post a comment (when it's `github-token` is specified) but it's
# not as useful, and this action cannot fail CI based on a minimum coverage threshold, which
# is why we use both in this way.
- name: Post coverage report
if: github.event_name == 'pull_request' # This action fails when ran outside of a pull request.
uses: romeovs/[email protected]
with:
delete-old-comments: true
lcov-file: ./lcov.info
github-token: ${{ secrets.GITHUB_TOKEN }} # Adds a coverage summary comment to the PR.

- name: Verify minimum coverage
uses: zgosalvez/github-actions-report-lcov@v2
with:
coverage-files: ./lcov.info
minimum-coverage: 95 # Set coverage threshold.
60 changes: 60 additions & 0 deletions .github/workflows/foundry-gas-diff.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Report gas diff

on:
push:
branches:
- main
pull_request:
# Optionally configure to run only for changes in specific files. For example:
paths:
- src/**
- test/**
- foundry.toml
- remappings.txt
- .github/workflows/foundry-gas-diff.yml

env:
FOUNDRY_PROFILE: default
INFURA_KEY: ${{ secrets.INFURA_KEY }}
ALCHEMY_KEY: ${{ secrets.ALCHEMY_KEY }}
GC_RPC: ${{ secrets.GC_RPC }}
POLYGON_RPC: ${{ secrets.POLYGON_RPC }}
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}

jobs:
compare_gas_reports:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

# Add any step generating a gas report to a temporary file named gasreport.ansi. For example:
- name: Run tests
run: forge test --gas-report > gasreport.ansi # <- this file name should be unique in your repository!
env:
# make fuzzing semi-deterministic to avoid noisy gas cost estimation
# due to non-deterministic fuzzing (but still use pseudo-random fuzzing seeds)
FOUNDRY_FUZZ_SEED: 0x${{ github.event.pull_request.base.sha || github.sha }}

- name: Compare gas reports
uses: Rubilmax/[email protected]
with:
summaryQuantile: 0.9 # only display the 10% most significant gas diffs in the summary (defaults to 20%)
sortCriteria: avg,max # sort diff rows by criteria
sortOrders: desc,asc # and directions
ignore: test-foundry/**/* # filter out gas reports from specific paths (test/ is included by default)
id: gas_diff

- name: Add gas diff to sticky comment
if: github.event_name == 'pull_request' || github.event_name == 'pull_request_target'
uses: marocchino/sticky-pull-request-comment@v2
with:
# delete the comment in case changes no longer impact gas costs
delete: ${{ !steps.gas_diff.outputs.markdown }}
message: ${{ steps.gas_diff.outputs.markdown }}
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
cache/
out/
optimized-out/
.vscode/
broadcast/
.env
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std

8 changes: 8 additions & 0 deletions .markdownlintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"MD024": false,
"MD013": false,
"MD026": false,
"MD028": false,
"MD033": false,
"MD046": false
}
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# solidity-template

Template repo for solidity projects

## Overview and Usage

## Development

This repo uses Foundry for development and testing. To get started:

1. Fork the project
2. Install [Foundry](https://book.getfoundry.sh/getting-started/installation)
3. To install dependencies, run `forge install`
4. To compile the contracts, run `forge build`
5. To test, run `forge test`

### IR-Optimized Builds

This repo also supports contracts compiled via IR. Since compiling all contracts via IR would slow down testing workflows, we only want to do this for our target contract(s), not anything in this `test` or `script` stack. We accomplish this by pre-compiled the target contract(s) and then loading the pre-compiled artifacts in the test suite.

First, we compile the target contract(s) via IR by running`FOUNDRY_PROFILE=optimized forge build` (ensuring that FOUNDRY_PROFILE is not in our .env file)

Next, ensure that tests are using the `DeployOptimized` script, and run `forge test` as normal.

See the wonderful [Seaport repo](https://github.com/ProjectOpenSea/seaport/blob/main/README.md#foundry-tests) for more details and options for this approach.
10 changes: 10 additions & 0 deletions example.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
GC_RPC=
POLYGON_RPC=
INFURA_KEY=
export PRIVATE_KEY=
ETHERSCAN_KEY=
GNOSISSCAN_KEY=
POLYGONSCAN_KEY=
OPTIMISM_KEY=
ARBISCAN_KEY=
FOUNDRY_PROFILE=
71 changes: 71 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
[profile.default]
src = 'src'
out = 'out'
libs = ['lib']
optimizer_runs = 1_000_000
# the following setting ensures that deterministically deployed contracts will always be to the same address
bytecode_hash = "none"
gas_reports = []
auto_detect_solc = false
solc = "0.8.19"
remappings = [
"ds-test/=lib/forge-std/lib/ds-test/src/",
"forge-std/=lib/forge-std/src/",
]
# Enable tests to read ir-optimized bytecode precompiled by profile.optimized
fs_permissions = [{ access = "read", path = "./optimized-out" }]

# for pre-compiling ir-optimized bytecode that will be later deployed by tests
[profile.optimized]
via_ir = true
out = "optimized-out"
script = "src"
bytecode_hash = "none"
sizes = true
# no need to compile tests with via-ir since they load optimized bytecode directly by default
test = "src"

# for running tests against pre-compiled ir-optimized deployments
[profile.test]
src = "test"

[profile.ci]
fuzz = { runs = 5000 }
invariant = { runs = 1000 }

[profile.lite]
fuzz = { runs = 32 }
invariant = { runs = 10 }
# Speed up compilation and tests during development.
optimizer = false

[fmt]
bracket_spacing = true
int_types = "long"
line_length = 120
multiline_func_header = "attributes_first"
number_underscore = "thousands"
quote_style = "double"
tab_width = 2
wrap_comments = true

[rpc_endpoints]
arbitrum = "https://arbitrum-mainnet.infura.io/v3/${INFURA_KEY}"
gnosis = "${GC_RPC}"
goerli = "https://goerli.infura.io/v3/${INFURA_KEY}"
local = "http://localhost:8545"
mainnet = "https://mainnet.infura.io/v3/${INFURA_KEY}"
optimism = "https://optimism-mainnet.infura.io/v3/${INFURA_KEY}"
polygon = "${POLYGON_RPC}"
sepolia = "https://sepolia.infura.io/v3/${INFURA_KEY}"

[etherscan]
arbitrum = { key = "${ARBISCAN_KEY}", url = "https://api.arbiscan.io/api" }
goerli = { key = "${ETHERSCAN_KEY}", url = "https://api-goerli.etherscan.io/api" }
gnosis = { key = "${GNOSISSCAN_KEY}", url = "https://api.gnosisscan.io/api" }
mainnet = { key = "${ETHERSCAN_KEY}", url = "https://api.etherscan.io/api" }
optimism = { key = "${OPTIMISM_KEY}", url = "https://api-optimistic.etherscan.io/api" }
sepolia = { key = "${ETHERSCAN_KEY}", url = "https://api-sepolia.etherscan.io/api" }
polygon = { key = "${POLYGONSCAN_KEY}", url = "https://api.polygonscan.com/api" }

# See more config options https://github.com/foundry-rs/foundry/tree/master/config
1 change: 1 addition & 0 deletions lib/forge-std
Submodule forge-std added at 1cefc0
98 changes: 98 additions & 0 deletions script/Counter.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import { Script, console2 } from "forge-std/Script.sol";
import { Counter } from "../src/Counter.sol";

interface ImmutableCreate2Factory {
function create2(bytes32 salt, bytes calldata initializationCode)
external
payable
returns (address deploymentAddress);
}

contract Deploy is Script {
Counter public counter;
bytes32 public SALT = bytes32(abi.encode("lets add some salt to these eggs"));

// default values
bool internal _verbose = true;

/// @dev Override default values, if desired
function prepare(bool verbose) public {
_verbose = verbose;
}

/// @dev Set up the deployer via their private key from the environment
function deployer() public returns (address) {
uint256 privKey = vm.envUint("PRIVATE_KEY");
return vm.rememberKey(privKey);
}

function _log(string memory prefix) internal view {
if (_verbose) {
console2.log(string.concat(prefix, "Counter:"), address(counter));
}
}

/// @dev Deploy the contract to a deterministic address via forge's create2 deployer factory.
function run() public virtual {
vm.startBroadcast(deployer());

/**
* @dev Deploy the contract to a deterministic address via forge's create2 deployer factory, which is at this
* address on all chains: `0x4e59b44847b379578588920cA78FbF26c0B4956C`.
* The resulting deployment address is determined by only two factors:
* 1. The bytecode hash of the contract to deploy. Setting `bytecode_hash` to "none" in foundry.toml ensures that
* never differs regardless of where its being compiled
* 2. The provided salt, `SALT`
*/
counter = new Counter{ salt: SALT}(/* insert constructor args here */);

vm.stopBroadcast();

_log("");
}
}

/// @dev Deploy pre-compiled ir-optimized bytecode to a non-deterministic address
contract DeployPrecompiled is Deploy {
/// @dev Update SALT and default values in Deploy contract

function run() public override {
vm.startBroadcast(deployer());

bytes memory args = abi.encode( /* insert constructor args here */ );

/// @dev Load and deploy pre-compiled ir-optimized bytecode.
counter = Counter(deployCode("optimized-out/Counter.sol/Counter.json", args));

vm.stopBroadcast();

_log("Precompiled ");
}
}

/* FORGE CLI COMMANDS
## A. Simulate the deployment locally
forge script script/Deploy.s.sol -f mainnet
## B. Deploy to real network and verify on etherscan
forge script script/Deploy.s.sol -f mainnet --broadcast --verify
## C. Fix verification issues (replace values in curly braces with the actual values)
forge verify-contract --chain-id 1 --num-of-optimizations 1000000 --watch --constructor-args $(cast abi-encode \
"constructor({args})" "{arg1}" "{arg2}" "{argN}" ) \
--compiler-version v0.8.19 {deploymentAddress} \
src/{Counter}.sol:{Counter} --etherscan-api-key $ETHERSCAN_KEY
## D. To verify ir-optimized contracts on etherscan...
1. Run (C) with the following additional flag: `--show-standard-json-input > etherscan.json`
2. Patch `etherscan.json`: `"optimizer":{"enabled":true,"runs":100}` =>
`"optimizer":{"enabled":true,"runs":100},"viaIR":true`
3. Upload the patched `etherscan.json` to etherscan manually
See this github issue for more: https://github.com/foundry-rs/foundry/issues/3507#issuecomment-1465382107
*/
Loading

0 comments on commit b6a68fd

Please sign in to comment.