diff --git a/.github/workflows/foundry.yml b/.github/workflows/foundry.yml new file mode 100644 index 0000000..dc51e28 --- /dev/null +++ b/.github/workflows/foundry.yml @@ -0,0 +1,75 @@ +# Tests compilation and E2E integration with foundry-polkadot, including basic +# runtime behavior, but not differential runtime correctness. + +name: Foundry Integration + +on: + workflow_call: + +env: + CARGO_TERM_COLOR: always + # Workflow-internal foundry envs are prefixed with `CI_` to not conflict with the + # `FOUNDRY_*` namespace, which foundry itself will interpret for its configuration. + CI_FOUNDRY_PROJECT_ROOT: tooling-projects/foundry/erc20 + CI_FOUNDRY_POLKADOT_VERSION: v1.5.1 + # Coincides with the forge-std revision pinned in foundry-polkadot/testdata/forge-std-rev. + FORGE_STD_VERSION: v1.16.1 + OZ_CONTRACTS_VERSION: v5.3.0 + +jobs: + build: + uses: ./.github/workflows/reusable-build-linux.yml + with: + retention_days: 1 + + use-foundry: + needs: build + runs-on: ubuntu-24.04 + steps: + - name: Checkout revive + uses: actions/checkout@v6 + + - name: Download resolc Binary + uses: actions/download-artifact@v7 + with: + name: resolc-x86_64-unknown-linux-musl + path: resolc-bin + + - name: Export resolc Path + run: | + chmod +x resolc-bin/resolc-x86_64-unknown-linux-musl + echo "RESOLC_PATH=$(pwd)/resolc-bin/resolc-x86_64-unknown-linux-musl" >> $GITHUB_ENV + + - name: Install foundryup-polkadot and the Foundry Toolchain + run: | + # Pin install path with `FOUNDRY_DIR` to prevent the pre-existing `XDG_CONFIG_HOME` from being used. + export FOUNDRY_DIR="$HOME/.foundry" + curl -fsSL https://raw.githubusercontent.com/paritytech/foundry-polkadot/refs/heads/master/foundryup/install | bash + "$FOUNDRY_DIR/bin/foundryup-polkadot" --install "$CI_FOUNDRY_POLKADOT_VERSION" + + - name: Add Foundry to PATH + run: echo "$HOME/.foundry/bin" >> "$GITHUB_PATH" + + - name: Check Tool Versions + run: | + foundryup-polkadot --version + forge --version + + - name: Fetch Project Dependencies + working-directory: ${{ env.CI_FOUNDRY_PROJECT_ROOT }} + run: | + mkdir -p lib + git clone --depth 1 --branch "$FORGE_STD_VERSION" https://github.com/foundry-rs/forge-std.git lib/forge-std + git clone --depth 1 --branch "$OZ_CONTRACTS_VERSION" https://github.com/OpenZeppelin/openzeppelin-contracts.git lib/openzeppelin-contracts + + - name: Compile Project + working-directory: ${{ env.CI_FOUNDRY_PROJECT_ROOT }} + run: forge build --use-resolc "$RESOLC_PATH" --optimize -Oz + + - name: Verify Output + working-directory: ${{ env.CI_FOUNDRY_PROJECT_ROOT }} + run: bash verify-compiler-output.sh "$RESOLC_PATH" + + - name: Test Project + working-directory: ${{ env.CI_FOUNDRY_PROJECT_ROOT }} + run: forge test --polkadot=pvm --use-resolc "$RESOLC_PATH" --optimize -Oz -vvvv diff --git a/.github/workflows/reusable-build-linux.yml b/.github/workflows/reusable-build-linux.yml index a6ee993..2ca06a7 100644 --- a/.github/workflows/reusable-build-linux.yml +++ b/.github/workflows/reusable-build-linux.yml @@ -28,6 +28,7 @@ jobs: # Use `reusable-build` and not `${{ github.workflow }}` here so that callers share the same concurrency group. group: reusable-build-x86_64-unknown-linux-musl-${{ github.sha }} cancel-in-progress: false + queue: max steps: - name: Checkout revive uses: actions/checkout@v6 diff --git a/.github/workflows/reusable-build-macos.yml b/.github/workflows/reusable-build-macos.yml index 435f622..cde9fc8 100644 --- a/.github/workflows/reusable-build-macos.yml +++ b/.github/workflows/reusable-build-macos.yml @@ -50,6 +50,7 @@ jobs: # Use `reusable-build` and not `${{ github.workflow }}` here so that callers share the same concurrency group. group: reusable-build-${{ matrix.target }}-${{ github.sha }} cancel-in-progress: false + queue: max steps: - name: Checkout revive uses: actions/checkout@v6 @@ -80,6 +81,7 @@ jobs: # Use `reusable-build` and not `${{ github.workflow }}` here so that callers share the same concurrency group. group: reusable-build-universal-apple-darwin-${{ github.sha }} cancel-in-progress: false + queue: max steps: - name: Check Build Cache id: cache diff --git a/.github/workflows/reusable-build-wasm.yml b/.github/workflows/reusable-build-wasm.yml index e6ea149..a3466d0 100644 --- a/.github/workflows/reusable-build-wasm.yml +++ b/.github/workflows/reusable-build-wasm.yml @@ -34,6 +34,7 @@ jobs: # Use `reusable-build` and not `${{ github.workflow }}` here so that callers share the same concurrency group. group: reusable-build-wasm32-unknown-emscripten-is-release-${{ inputs.is_release }}-${{ github.sha }} cancel-in-progress: false + queue: max steps: - name: Checkout revive uses: actions/checkout@v6 diff --git a/.github/workflows/reusable-build-windows.yml b/.github/workflows/reusable-build-windows.yml index b616162..4a4a393 100644 --- a/.github/workflows/reusable-build-windows.yml +++ b/.github/workflows/reusable-build-windows.yml @@ -28,6 +28,7 @@ jobs: # Use `reusable-build` and not `${{ github.workflow }}` here so that callers share the same concurrency group. group: reusable-build-x86_64-pc-windows-msvc-${{ github.sha }} cancel-in-progress: false + queue: max steps: - name: Enable Long Paths run: git config --system core.longpaths true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4e91248..4d9f8bc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -64,6 +64,7 @@ jobs: outputs: should-run-differential-tests: ${{ steps.filter.outputs.should-run-differential-tests }} should-run-hardhat-tests: ${{ steps.filter.outputs.should-run-hardhat-tests }} + should-run-foundry-tests: ${{ steps.filter.outputs.should-run-foundry-tests }} steps: - name: Checkout uses: actions/checkout@v6 @@ -95,23 +96,28 @@ jobs: print_changed_files + common_ignored=( + '\.md$' + '^book/' + '^docs/' + '^js/' + '^package\.json$' + '^package-lock\.json$' + ) + echo "should-run-differential-tests=$(has_changes_other_than \ - '\.md$' \ - '^book/' \ - '^docs/' \ - '^js/' \ - '^package\.json$' \ - '^package-lock\.json$' \ + "${common_ignored[@]}" \ '^tooling-projects/' \ )" | tee -a "$GITHUB_OUTPUT" echo "should-run-hardhat-tests=$(has_changes_other_than \ - '\.md$' \ - '^book/' \ - '^docs/' \ - '^js/' \ - '^package\.json$' \ - '^package-lock\.json$' \ + "${common_ignored[@]}" \ + '^tooling-projects/foundry' \ + )" | tee -a "$GITHUB_OUTPUT" + + echo "should-run-foundry-tests=$(has_changes_other_than \ + "${common_ignored[@]}" \ + '^tooling-projects/hardhat' \ )" | tee -a "$GITHUB_OUTPUT" run-differential-tests: @@ -123,3 +129,8 @@ jobs: needs: [test, check-changes] if: ${{ needs.check-changes.outputs.should-run-hardhat-tests == 'true' }} uses: ./.github/workflows/hardhat.yml + + run-foundry-tests: + needs: [test, check-changes] + if: ${{ needs.check-changes.outputs.should-run-foundry-tests == 'true' }} + uses: ./.github/workflows/foundry.yml diff --git a/tooling-projects/foundry/erc20/foundry.toml b/tooling-projects/foundry/erc20/foundry.toml new file mode 100644 index 0000000..59ce13d --- /dev/null +++ b/tooling-projects/foundry/erc20/foundry.toml @@ -0,0 +1,25 @@ +[profile.default] +src = "src" +test = "test" +libs = ["lib"] +solc = "0.8.35" +remappings = [ + "forge-std/=lib/forge-std/src/", + "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", +] +extra_output = [ + "abi", + "metadata", + "devdoc", + "userdoc", + "storageLayout", + "ir", + "irOptimized", + "evm.bytecode", + "evm.deployedBytecode", + "evm.assembly", + "evm.methodIdentifiers", +] + +[profile.default.polkadot] +resolc_compile = true diff --git a/tooling-projects/foundry/erc20/src/MyToken.sol b/tooling-projects/foundry/erc20/src/MyToken.sol new file mode 100644 index 0000000..da68dc5 --- /dev/null +++ b/tooling-projects/foundry/erc20/src/MyToken.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +// Compatible with OpenZeppelin Contracts ^5.0.0 +pragma solidity ^0.8.22; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +contract MyToken is ERC20, Ownable { + constructor(address initialOwner) + ERC20("MyToken", "MTK") + Ownable(initialOwner) + {} + + function mint(address to, uint256 amount) public onlyOwner { + _mint(to, amount); + } +} diff --git a/tooling-projects/foundry/erc20/test/MyToken.t.sol b/tooling-projects/foundry/erc20/test/MyToken.t.sol new file mode 100644 index 0000000..ed668f6 --- /dev/null +++ b/tooling-projects/foundry/erc20/test/MyToken.t.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import { Test } from "forge-std/Test.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { MyToken } from "../src/MyToken.sol"; + +contract MyTokenTest is Test { + MyToken internal token; + address internal owner = address(this); + address internal alice = address(0xA11CE); + address internal bob = address(0xB0B); + + function setUp() public { + token = new MyToken(owner); + } + + function test_NameAndSymbol() public view { + assertEq(token.name(), "MyToken"); + assertEq(token.symbol(), "MTK"); + } + + function test_Owner() public view { + assertEq(token.owner(), owner); + } + + function test_OwnerCanMint() public { + token.mint(alice, 1000); + assertEq(token.balanceOf(alice), 1000); + } + + function test_NonOwnerCannotMint() public { + vm.prank(alice); + vm.expectRevert( + abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice) + ); + token.mint(alice, 1000); + } + + function test_TotalSupplyIncreases() public { + uint256 before = token.totalSupply(); + token.mint(alice, 500); + assertEq(token.totalSupply() - before, 500); + } + + function test_Transfer() public { + token.mint(owner, 1000); + assertTrue(token.transfer(alice, 100)); + assertEq(token.balanceOf(alice), 100); + assertEq(token.balanceOf(owner), 900); + } + + function test_TransferFrom() public { + token.mint(owner, 1000); + token.approve(alice, 50); + vm.prank(alice); + assertTrue(token.transferFrom(owner, bob, 50)); + assertEq(token.balanceOf(bob), 50); + assertEq(token.allowance(owner, alice), 0); + } +} diff --git a/tooling-projects/foundry/erc20/verify-compiler-output.sh b/tooling-projects/foundry/erc20/verify-compiler-output.sh new file mode 100644 index 0000000..89530f2 --- /dev/null +++ b/tooling-projects/foundry/erc20/verify-compiler-output.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +# Asserts that the compiled project contains the expected compiler output. +# Requires `forge` in PATH. Run from the project's root. +# +# Usage: verify-compiler-output.sh + +set -euxo pipefail + +if [[ $# -ne 1 ]]; then + echo "usage: $(basename "$0") " >&2 + exit 2 +fi + +resolc=$1 + +inspect() { + forge inspect --use-resolc "$resolc" MyToken "$@" +} + +inspect bytecode | grep '^0x50564d' > /dev/null +inspect deployedBytecode | grep '^0x50564d' > /dev/null +inspect irOptimized | grep . > /dev/null +inspect ir | grep . > /dev/null +inspect assembly | grep . > /dev/null +inspect abi --json | jq -e 'length > 0' > /dev/null +inspect methodIdentifiers --json | jq -e 'length > 0' > /dev/null +inspect storageLayout --json | jq -e '.storage | length > 0' > /dev/null +inspect metadata --json | jq -e 'length > 0' > /dev/null +inspect devdoc --json | jq -e 'length > 0' > /dev/null +inspect userdoc --json | jq -e 'length > 0' > /dev/null + +echo "all checks passed"