diff --git a/.github/workflows/foundry.yml b/.github/workflows/foundry.yml new file mode 100644 index 00000000..042207fa --- /dev/null +++ b/.github/workflows/foundry.yml @@ -0,0 +1,91 @@ +# 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 + # This is currently the commit that adds support for resolc ">=0.6.0, <2.0.0". + # The latest release as of Apr. 28, 2026 supports ">=0.6.0, <0.7.0". + # Once a release includes the updated supported versions, this can be changed to a + # tag in order to download the prebuilt binary. + CI_FOUNDRY_POLKADOT_COMMIT: b3173d0584382687cc96b2af072d8cb2addf23d3 + +jobs: + use-foundry: + runs-on: ubuntu-24.04 + steps: + - name: Checkout revive + uses: actions/checkout@v4 + + - name: Set Up Rust Toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + # without this it will override our rust flags + rustflags: "" + # TODO: Remove once the foundry install step replaces `--commit` with `--install`. + # Needed for building foundry-polkadot. + target: wasm32-unknown-unknown + components: rust-src + + - name: Download resolc Binary + uses: actions/download-artifact@v4 + 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 + + # TODO: Remove once the foundry install step replaces `--commit` with `--install`. + - name: Install Build Dependencies + run: | + sudo apt-get update + sudo apt-get install -y clang libclang-dev build-essential protobuf-compiler + + # NOTE: ~/.foundry/bin is not cached (which would be very useful when using `--commit`) + # due to forge sometimes crashing with `Illegal instruction` when using the cached + # binaries, seemingly due to the CPU being different when building forge vs running it. + - name: Install foundryup-polkadot and the Foundry Toolchain + run: | + # TODO: Replace `--commit` with `--install` once a release with this commit exists: https://github.com/paritytech/foundry-polkadot/commit/b3173d0584382687cc96b2af072d8cb2addf23d3 + + # 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" --commit "$CI_FOUNDRY_POLKADOT_COMMIT" + + - 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 v1.15.0 https://github.com/foundry-rs/forge-std.git lib/forge-std + git clone --depth 1 --branch v5.3.0 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/test.yml b/.github/workflows/test.yml index 427600e1..c40148f3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -65,3 +65,7 @@ jobs: # Only run this if `test` successfully completes. needs: test uses: ./.github/workflows/differential-tests.yml + + run-foundry-tests: + needs: run-differential-tests + 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 00000000..59ce13da --- /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 00000000..da68dc5c --- /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 00000000..ed668f60 --- /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 00000000..89530f2b --- /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"