├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── BUG-FORM.yml │ ├── FEATURE-FORM.yml │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md ├── changelog.json ├── compilation-benchmark.png ├── demo.gif ├── logo.png ├── scripts │ ├── create-tag.js │ ├── move-tag.js │ └── prune-prereleases.js └── workflows │ ├── cross-platform.yml │ ├── docker-publish.yml │ ├── project.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── binder ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ └── utils.rs ├── cast ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ ├── print_utils.rs │ └── tx.rs ├── cli ├── Cargo.toml ├── README.md ├── assets │ ├── .gitignoreTemplate │ ├── ContractTemplate.sol │ ├── ContractTemplate.t.sol │ └── workflowTemplate.yml ├── build.rs ├── src │ ├── cast.rs │ ├── cmd │ │ ├── cast │ │ │ ├── find_block.rs │ │ │ └── mod.rs │ │ ├── forge │ │ │ ├── bind.rs │ │ │ ├── build.rs │ │ │ ├── certora.rs │ │ │ ├── config.rs │ │ │ ├── create.rs │ │ │ ├── flatten.rs │ │ │ ├── fmt.rs │ │ │ ├── init.rs │ │ │ ├── inspect.rs │ │ │ ├── install.rs │ │ │ ├── mod.rs │ │ │ ├── remappings.rs │ │ │ ├── run.rs │ │ │ ├── snapshot.rs │ │ │ ├── test.rs │ │ │ ├── tree.rs │ │ │ ├── verify.rs │ │ │ └── watch.rs │ │ ├── mod.rs │ │ └── utils.rs │ ├── compile.rs │ ├── forge.rs │ ├── opts │ │ ├── cast.rs │ │ ├── forge.rs │ │ └── mod.rs │ ├── term.rs │ └── utils.rs ├── test-utils │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── macros.rs │ │ └── util.rs └── tests │ └── it │ ├── cast.rs │ ├── cmd.rs │ ├── config.rs │ ├── integration.rs │ ├── main.rs │ ├── test.rs │ └── verify.rs ├── common ├── Cargo.toml ├── README.md └── src │ ├── evm.rs │ └── lib.rs ├── config ├── Cargo.toml ├── README.md └── src │ ├── caching.rs │ ├── chain.rs │ ├── lib.rs │ ├── macros.rs │ └── utils.rs ├── evm ├── Cargo.toml ├── abi │ └── console.json ├── src │ ├── debug.rs │ ├── decode.rs │ ├── executor │ │ ├── abi.rs │ │ ├── builder.rs │ │ ├── fork │ │ │ ├── backend.rs │ │ │ ├── cache.rs │ │ │ ├── init.rs │ │ │ └── mod.rs │ │ ├── inspector │ │ │ ├── cheatcodes │ │ │ │ ├── env.rs │ │ │ │ ├── expect.rs │ │ │ │ ├── ext.rs │ │ │ │ ├── fuzz.rs │ │ │ │ ├── mod.rs │ │ │ │ └── util.rs │ │ │ ├── debugger.rs │ │ │ ├── logs.rs │ │ │ ├── mod.rs │ │ │ ├── stack.rs │ │ │ ├── tracer.rs │ │ │ └── utils.rs │ │ ├── mod.rs │ │ └── opts.rs │ ├── fuzz │ │ ├── mod.rs │ │ └── strategies │ │ │ ├── calldata.rs │ │ │ ├── mod.rs │ │ │ ├── param.rs │ │ │ ├── state.rs │ │ │ └── uint.rs │ ├── lib.rs │ └── trace │ │ ├── decoder.rs │ │ ├── identifier │ │ ├── etherscan.rs │ │ ├── local.rs │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── node.rs │ │ └── utils.rs └── test-data │ └── storage.json ├── fmt ├── Cargo.toml ├── README.md ├── src │ ├── formatter.rs │ ├── lib.rs │ ├── loc.rs │ └── visit.rs └── testdata │ ├── ContractDefinitions │ ├── bracket-spacing.fmt.sol │ ├── fmt.sol │ └── original.sol │ ├── EnumDefinitions │ ├── bracket-spacing.fmt.sol │ ├── fmt.sol │ └── original.sol │ ├── ImportDirective │ ├── bracket-spacing.fmt.sol │ ├── fmt.sol │ └── original.sol │ ├── StatementBlock │ ├── bracket-spacing.fmt.sol │ ├── fmt.sol │ └── original.sol │ ├── StructDefinition │ ├── bracket-spacing.fmt.sol │ ├── fmt.sol │ └── original.sol │ └── TypeDefinition │ ├── fmt.sol │ └── original.sol ├── forge ├── Cargo.toml ├── README.md └── src │ ├── gas_report.rs │ ├── lib.rs │ ├── multi_runner.rs │ └── runner.rs ├── foundryup ├── README.md ├── foundryup └── install ├── rust-toolchain ├── rustfmt.toml ├── testdata ├── README.md ├── cheats │ ├── Addr.t.sol │ ├── Assume.t.sol │ ├── Cheats.sol │ ├── Deal.t.sol │ ├── Etch.t.sol │ ├── ExpectCall.t.sol │ ├── ExpectEmit.t.sol │ ├── ExpectRevert.t.sol │ ├── Fee.t.sol │ ├── Ffi.t.sol │ ├── GetCode.t.sol │ ├── GetNonce.t.sol │ ├── Label.t.sol │ ├── Load.t.sol │ ├── MockCall.t.sol │ ├── Prank.t.sol │ ├── Record.t.sol │ ├── Roll.t.sol │ ├── SetNonce.t.sol │ ├── Setup.t.sol │ ├── Sign.t.sol │ ├── Store.t.sol │ ├── Travel.t.sol │ └── Warp.t.sol ├── core │ ├── Abstract.t.sol │ ├── DSStyle.t.sol │ ├── DappToolsParity.t.sol │ ├── FailingSetup.t.sol │ ├── LibraryLinking.t.sol │ ├── MultipleSetup.t.sol │ ├── PaymentFailure.t.sol │ ├── Reverting.t.sol │ └── SetupConsistency.t.sol ├── fixtures │ ├── GetCode │ │ ├── UnlinkedContract.sol │ │ ├── WorkingContract.json │ │ └── WorkingContract.sol │ └── SolidityGeneration │ │ ├── GeneratedNamedInterface.sol │ │ ├── GeneratedUnnamedInterface.sol │ │ └── InterfaceABI.json ├── fork │ ├── DssExecLib.sol │ └── Fork.t.sol ├── fuzz │ ├── Fuzz.t.sol │ └── FuzzNumbers.t.sol ├── lib │ └── ds-test │ │ └── src │ │ └── test.sol ├── logs │ ├── DebugLogs.t.sol │ └── HardhatLogs.t.sol └── trace │ ├── ConflictingSignatures.t.sol │ └── Trace.t.sol ├── ui ├── Cargo.toml └── src │ ├── lib.rs │ └── op_effects.rs └── utils ├── Cargo.toml ├── README.md └── src └── lib.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | .github 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG-FORM.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: File a bug report 3 | labels: ["T-bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Please ensure that the bug has not already been filed in the issue tracker. 9 | 10 | Thanks for taking the time to report this bug! 11 | - type: dropdown 12 | attributes: 13 | label: Component 14 | description: What component is the bug in? 15 | multiple: true 16 | options: 17 | - Forge 18 | - Cast 19 | - Foundryup 20 | - Other (please describe) 21 | validations: 22 | required: true 23 | - type: checkboxes 24 | attributes: 25 | label: Have you ensured that all of these are up to date? 26 | options: 27 | - label: Foundry 28 | - label: Foundryup 29 | validations: 30 | required: true 31 | - type: input 32 | attributes: 33 | label: What version of Foundry are you on? 34 | placeholder: "Run forge --version and paste the output here" 35 | - type: input 36 | attributes: 37 | label: What command(s) is the bug in? 38 | description: Leave empty if not relevant 39 | placeholder: "For example: forge test" 40 | - type: dropdown 41 | attributes: 42 | label: Operating System 43 | description: What operating system are you on? 44 | options: 45 | - Windows 46 | - macOS (amd) 47 | - macOS (M1) 48 | - Linux 49 | - type: textarea 50 | attributes: 51 | label: Describe the bug 52 | description: Please include relevant Solidity snippets as well if relevant. 53 | validations: 54 | required: true 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE-FORM.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest a feature 3 | labels: ["T-feature"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Please ensure that the feature has not already been requested in the issue tracker. 9 | 10 | Thanks for helping us improve Foundry! 11 | - type: dropdown 12 | attributes: 13 | label: Component 14 | description: What component is the feature for? 15 | multiple: true 16 | options: 17 | - Forge 18 | - Cast 19 | - Foundryup 20 | - Other (please describe) 21 | validations: 22 | required: true 23 | - type: textarea 24 | attributes: 25 | label: Describe the feature you would like 26 | description: Please also describe what the feature is aiming to solve, if relevant. 27 | validations: 28 | required: true 29 | - type: textarea 30 | attributes: 31 | label: Additional context 32 | description: Add any other context to the feature (like screenshots, resources) 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Support 4 | url: https://t.me/foundry_support 5 | about: This issue tracker is only for bugs and feature requests. Support is available on Telegram! 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | ## Motivation 9 | 10 | 15 | 16 | ## Solution 17 | 18 | 22 | -------------------------------------------------------------------------------- /.github/changelog.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [{ 3 | "title": "## Features", 4 | "labels": ["T-feature"] 5 | }, { 6 | "title": "## Fixes", 7 | "labels": ["T-bug", "T-fix"] 8 | }], 9 | "ignore_labels": [ 10 | "L-ignore" 11 | ], 12 | "template": "${{CHANGELOG}}\n## Other\n\n${{UNCATEGORIZED}}", 13 | "pr_template": "- ${{TITLE}} (#${{NUMBER}})", 14 | "empty_template": "- No changes" 15 | } 16 | -------------------------------------------------------------------------------- /.github/compilation-benchmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Certora/FoundryIntegration/0df41089c3605f9c4918a8e9b7652625fbba7c53/.github/compilation-benchmark.png -------------------------------------------------------------------------------- /.github/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Certora/FoundryIntegration/0df41089c3605f9c4918a8e9b7652625fbba7c53/.github/demo.gif -------------------------------------------------------------------------------- /.github/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Certora/FoundryIntegration/0df41089c3605f9c4918a8e9b7652625fbba7c53/.github/logo.png -------------------------------------------------------------------------------- /.github/scripts/create-tag.js: -------------------------------------------------------------------------------- 1 | module.exports = async ({github, context}, tagName) => { 2 | try { 3 | await github.rest.git.createRef({ 4 | owner: context.repo.owner, 5 | repo: context.repo.repo, 6 | ref: `refs/tags/${tagName}`, 7 | sha: context.sha, 8 | force: true 9 | }) 10 | } catch (err) { 11 | console.error(`Failed to create tag: ${tagName}`) 12 | console.error(err) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/scripts/move-tag.js: -------------------------------------------------------------------------------- 1 | module.exports = async ({github, context}, tagName) => { 2 | try { 3 | await github.rest.git.updateRef({ 4 | owner: context.repo.owner, 5 | repo: context.repo.repo, 6 | ref: `tags/${tagName}`, 7 | sha: context.sha, 8 | force: true 9 | }) 10 | } catch (err) { 11 | console.error(`Failed to move nightly tag.`) 12 | console.error(`This should only happen the first time.`) 13 | console.error(err) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/scripts/prune-prereleases.js: -------------------------------------------------------------------------------- 1 | module.exports = async ({github, context}) => { 2 | console.log('Pruning old prereleases') 3 | 4 | const { data: releases } = await github.rest.repos.listReleases({ 5 | owner: context.repo.owner, 6 | repo: context.repo.repo 7 | }) 8 | 9 | // Only consider releases tagged `nightly-${SHA}` for deletion 10 | let nightlies = releases.filter( 11 | (release) => release.tag_name.includes('nightly') && release.tag_name !== 'nightly' 12 | ) 13 | 14 | // Keep newest 3 nightlies 15 | nightlies = nightlies.slice(3) 16 | 17 | for (const nightly of nightlies) { 18 | console.log(`Deleting nightly: ${nightly.tag_name}`) 19 | await github.rest.repos.deleteRelease({ 20 | owner: context.repo.owner, 21 | repo: context.repo.repo, 22 | release_id: nightly.id 23 | }) 24 | console.log(`Deleting nightly tag: ${nightly.tag_name}`) 25 | await github.rest.git.deleteRef({ 26 | owner: context.repo.owner, 27 | repo: context.repo.repo, 28 | ref: `tags/${nightly.tag_name}` 29 | }) 30 | } 31 | 32 | console.log('Done.') 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/cross-platform.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | workflow_call: 4 | 5 | name: cross-platform 6 | 7 | jobs: 8 | build: 9 | name: ${{ matrix.job.target }} (${{ matrix.job.os }}) 10 | runs-on: ${{ matrix.job.os }} 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | job: 15 | # The OS is used for the runner 16 | # The target is used by Cargo 17 | - os: ubuntu-latest 18 | target: x86_64-unknown-linux-gnu 19 | - os: ubuntu-latest 20 | target: aarch64-unknown-linux-gnu 21 | - os: macos-latest 22 | target: x86_64-apple-darwin 23 | - os: macos-latest 24 | target: aarch64-apple-darwin 25 | #- os: windows-latest 26 | # target: x86_64-pc-windows-msvc 27 | 28 | steps: 29 | - name: Checkout sources 30 | uses: actions/checkout@v2 31 | 32 | - name: Apple M1 setup 33 | if: ${{ matrix.job.target == 'aarch64-apple-darwin' }} 34 | run: | 35 | echo "SDKROOT=$(xcrun -sdk macosx --show-sdk-path)" >> $GITHUB_ENV 36 | echo "MACOSX_DEPLOYMENT_TARGET=$(xcrun -sdk macosx --show-sdk-platform-version)" >> $GITHUB_ENV 37 | 38 | - name: Linux ARM setup 39 | if: ${{ matrix.job.target == 'aarch64-unknown-linux-gnu' }} 40 | run: | 41 | sudo apt-get update -y 42 | sudo apt-get install -y gcc-aarch64-linux-gnu 43 | echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV 44 | 45 | - name: Install Rust toolchain 46 | uses: actions-rs/toolchain@v1 47 | with: 48 | toolchain: stable 49 | target: ${{ matrix.job.target }} 50 | override: true 51 | profile: minimal 52 | 53 | - name: Environment information 54 | shell: bash 55 | run: | 56 | gcc --version || true 57 | rustup -V 58 | rustup toolchain list 59 | rustup default 60 | cargo -V 61 | rustc -V 62 | 63 | - uses: Swatinem/rust-cache@v1 64 | with: 65 | cache-on-failure: true 66 | 67 | - name: Build 68 | uses: actions-rs/cargo@v1 69 | with: 70 | use-cross: ${{ matrix.job.use-cross }} 71 | command: build 72 | args: --verbose --workspace 73 | 74 | - name: Run forge 75 | uses: actions-rs/cargo@v1 76 | with: 77 | use-cross: ${{ matrix.job.use-cross }} 78 | command: run 79 | args: -p foundry-cli --bin forge -- --help 80 | 81 | - name: Run tests 82 | uses: actions-rs/cargo@v1 83 | with: 84 | use-cross: ${{ matrix.job.use-cross }} 85 | command: test 86 | args: --verbose --workspace 87 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish Docker 2 | 3 | on: 4 | workflow_dispatch: 5 | workflow_call: 6 | 7 | env: 8 | REGISTRY: ghcr.io 9 | # Will resolve to gakonst/foundry 10 | IMAGE_NAME: ${{ github.repository }} 11 | 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: read 19 | packages: write 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v2 24 | 25 | # Login against a Docker registry except on PR 26 | # https://github.com/docker/login-action 27 | - name: Log into registry ${{ env.REGISTRY }} 28 | uses: docker/login-action@v1.14.1 29 | with: 30 | registry: ${{ env.REGISTRY }} 31 | username: ${{ github.actor }} 32 | password: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | # Extract metadata (tags, labels) for Docker 35 | # https://github.com/docker/metadata-action 36 | - name: Extract Docker metadata 37 | id: meta 38 | uses: docker/metadata-action@v3.6.2 39 | with: 40 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 41 | 42 | # Creates an additional 'latest' or 'nightly' tag 43 | # If the job is triggered via cron schedule, tag nightly and nightly-{SHA} 44 | # If the job is triggered via workflow dispatch and on a master branch, tag branch and latest 45 | # Otherwise, just tag as the branch name 46 | - name: Finalize Docker Metadata 47 | id: docker_tagging 48 | run: | 49 | if [[ "${{ github.event_name }}" == 'schedule' ]]; then 50 | echo "cron trigger, assigning nightly tag" 51 | echo "::set-output name=docker_tags::${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:nightly,${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:nightly-${GITHUB_SHA}" 52 | elif [[ "${GITHUB_REF##*/}" == "main" ]] || [[ ${GITHUB_REF##*/} == "master" ]]; then 53 | echo "manual trigger from master/main branch, assigning latest tag" 54 | echo "::set-output name=docker_tags::${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${GITHUB_REF##*/},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" 55 | else 56 | echo "Neither scheduled nor manual release from main branch. Just tagging as branch name" 57 | echo "::set-output name=docker_tags::${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${GITHUB_REF##*/}" 58 | fi 59 | 60 | # Log docker metadata to explicitly know what is being pushed 61 | - name: Inspect Docker Metadata 62 | run: | 63 | echo "TAGS -> ${{ steps.docker_tagging.outputs.docker_tags }}" 64 | echo "LABELS -> ${{ steps.meta.outputs.labels }}" 65 | 66 | # Build and push Docker image 67 | # https://github.com/docker/build-push-action 68 | - name: Build and push Docker image 69 | id: build-and-push 70 | uses: docker/build-push-action@v2.10.0 71 | with: 72 | context: . 73 | push: true 74 | tags: ${{ steps.docker_tagging.outputs.docker_tags }} 75 | labels: ${{ steps.meta.outputs.labels }} 76 | -------------------------------------------------------------------------------- /.github/workflows/project.yml: -------------------------------------------------------------------------------- 1 | name: project 2 | on: 3 | issues: 4 | types: [opened, transferred] 5 | 6 | jobs: 7 | add-to-project: 8 | name: add issue 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/add-to-project@main 12 | with: 13 | project-url: https://github.com/orgs/foundry-rs/projects/2 14 | github-token: ${{ secrets.GH_PROJECTS_TOKEN }} 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | 7 | name: test 8 | 9 | jobs: 10 | unit: 11 | name: unit tests 12 | runs-on: ubuntu-latest 13 | env: 14 | ETH_RPC_URL: https://eth-mainnet.alchemyapi.io/v2/Lc7oIGYeL_QvInzI0Wiu_pOZZDEKBrdf 15 | steps: 16 | - name: Checkout sources 17 | uses: actions/checkout@v2 18 | 19 | - name: Install toolchain 20 | uses: actions-rs/toolchain@v1 21 | with: 22 | toolchain: stable 23 | profile: minimal 24 | override: true 25 | - uses: Swatinem/rust-cache@v1 26 | with: 27 | cache-on-failure: true 28 | 29 | - name: cargo test 30 | run: cargo test --locked --workspace --all-features --lib --bins 31 | 32 | doc: 33 | name: doc tests 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Checkout sources 37 | uses: actions/checkout@v2 38 | - name: Install toolchain 39 | uses: actions-rs/toolchain@v1 40 | with: 41 | toolchain: stable 42 | profile: minimal 43 | override: true 44 | - uses: Swatinem/rust-cache@v1 45 | with: 46 | cache-on-failure: true 47 | 48 | - name: cargo test 49 | run: cargo test --locked --workspace --all-features --doc 50 | 51 | integration: 52 | name: integration tests 53 | runs-on: ubuntu-latest 54 | steps: 55 | - name: Checkout sources 56 | uses: actions/checkout@v2 57 | 58 | - name: Install toolchain 59 | uses: actions-rs/toolchain@v1 60 | with: 61 | toolchain: stable 62 | profile: minimal 63 | override: true 64 | - name: Rust cache 65 | uses: Swatinem/rust-cache@v1 66 | with: 67 | cache-on-failure: true 68 | 69 | - name: cargo test 70 | run: cargo test --locked --workspace --test '*' 71 | 72 | external-integration: 73 | name: external integration tests 74 | runs-on: ubuntu-latest 75 | env: 76 | ETH_RPC_URL: https://eth-mainnet.alchemyapi.io/v2/Lc7oIGYeL_QvInzI0Wiu_pOZZDEKBrdf 77 | steps: 78 | - name: Checkout sources 79 | uses: actions/checkout@v2 80 | 81 | - name: Install toolchain 82 | uses: actions-rs/toolchain@v1 83 | with: 84 | toolchain: stable 85 | profile: minimal 86 | override: true 87 | - uses: Swatinem/rust-cache@v1 88 | with: 89 | cache-on-failure: true 90 | - name: Forge RPC cache 91 | uses: actions/cache@v3 92 | with: 93 | path: ~/.foundry/cache 94 | key: rpc-cache-${{ hashFiles('cli/tests/it/integration.rs') }} 95 | 96 | - name: Force use of HTTPS for submodules 97 | run: git config --global url."https://github.com/".insteadOf "git@github.com:" 98 | 99 | - name: cargo test 100 | run: cargo test --locked --workspace --features external-integration-tests --test '*' 101 | 102 | lint: 103 | runs-on: ubuntu-latest 104 | steps: 105 | - name: Checkout sources 106 | uses: actions/checkout@v2 107 | 108 | - name: Install toolchain 109 | uses: actions-rs/toolchain@v1 110 | with: 111 | # nightly regression https://github.com/rust-lang/rust/issues/95267 112 | toolchain: nightly-2022-03-20 113 | profile: minimal 114 | components: rustfmt, clippy 115 | override: true 116 | 117 | - uses: Swatinem/rust-cache@v1 118 | with: 119 | cache-on-failure: true 120 | 121 | - name: cargo fmt 122 | run: cargo +nightly-2022-03-20 fmt --all -- --check 123 | 124 | - name: cargo clippy 125 | run: cargo +nightly-2022-03-20 clippy --all --all-features -- -D warnings 126 | 127 | cross-platform: 128 | name: Cross-platform tests 129 | if: github.event_name != 'pull_request' 130 | needs: [integration, lint, doc, unit] 131 | uses: ./.github/workflows/cross-platform.yml 132 | 133 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | /target 3 | out/ 4 | out.json 5 | 6 | fmt/testdata/* 7 | .vscode 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "cast", 4 | "binder", 5 | "cli", 6 | "cli/test-utils", 7 | "common", 8 | "config", 9 | "evm", 10 | "fmt", 11 | "forge", 12 | "ui", 13 | "utils", 14 | ] 15 | 16 | [profile.dev] 17 | # Disabling debug info speeds up builds a bunch, 18 | # and we don't rely on it for debugging that much 19 | debug = 0 20 | 21 | [profile.dev.package] 22 | # These speed up local tests 23 | ethers-solc.opt-level = 3 24 | 25 | # local "release" mode 26 | [profile.local] 27 | inherits = "dev" 28 | opt-level = 3 29 | # Set this to 1 or 2 to get more useful backtraces 30 | debug = 0 31 | panic = 'unwind' 32 | # better recompile times 33 | incremental = true 34 | codegen-units = 16 35 | 36 | 37 | [profile.release] 38 | # Optimize for binary size, but keep loop vectorization 39 | opt-level = "s" 40 | strip = true 41 | # Performance optimizations 42 | lto = "fat" 43 | codegen-units = 1 44 | panic = "abort" 45 | # We end up stripping away these symbols anyway 46 | debug = 0 47 | 48 | ## Patch ethers-rs with a local checkout then run `cargo update -p ethers` 49 | #[patch."https://github.com/gakonst/ethers-rs"] 50 | #ethers = { path = "../ethers-rs" } 51 | #ethers-core = { path = "../ethers-rs/ethers-core" } 52 | #ethers-providers = { path = "../ethers-rs/ethers-providers" } 53 | #ethers-signers = { path = "../ethers-rs/ethers-signers" } 54 | #ethers-etherscan = { path = "../ethers-rs/ethers-etherscan" } 55 | #ethers-solc = { path = "../ethers-rs/ethers-solc" } 56 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | from alpine as build-environment 2 | WORKDIR /opt 3 | RUN apk add clang lld curl build-base linux-headers \ 4 | && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh \ 5 | && chmod +x ./rustup.sh \ 6 | && ./rustup.sh -y 7 | WORKDIR /opt/foundry 8 | COPY . . 9 | RUN source $HOME/.profile && cargo build --release \ 10 | && strip /opt/foundry/target/release/forge \ 11 | && strip /opt/foundry/target/release/cast 12 | 13 | from alpine as foundry-client 14 | ENV GLIBC_KEY=https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub 15 | ENV GLIBC_KEY_FILE=/etc/apk/keys/sgerrand.rsa.pub 16 | ENV GLIBC_RELEASE=https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.35-r0/glibc-2.35-r0.apk 17 | 18 | RUN apk add linux-headers gcompat 19 | RUN wget -q -O ${GLIBC_KEY_FILE} ${GLIBC_KEY} \ 20 | && wget -O glibc.apk ${GLIBC_RELEASE} \ 21 | && apk add glibc.apk --force 22 | COPY --from=build-environment /opt/foundry/target/release/forge /usr/local/bin/forge 23 | COPY --from=build-environment /opt/foundry/target/release/cast /usr/local/bin/cast 24 | ENTRYPOINT ["/bin/sh", "-c"] -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Georgios Konstantopoulos 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE O THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE.R 26 | -------------------------------------------------------------------------------- /binder/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foundry-binder" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | readme = "./README.md" 7 | description = """ 8 | Generate rust bindings for solidity projects 9 | """ 10 | repository = "https://github.com/gakonst/foundry" 11 | keywords = ["ethereum", "web3", "solidity"] 12 | 13 | 14 | [dependencies] 15 | foundry-config = { path = "../config" } 16 | ethers-solc = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["async", "svm-solc", "project-util"] } 17 | ethers-contract = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["abigen"] } 18 | curl = { version = "0.4", default-features = false, features = ["http2"] } 19 | eyre = "0.6" 20 | git2 = { version = "0.13", default-features = false } 21 | url = "2.2" 22 | tracing = "0.1.33" 23 | tempfile = "3.3.0" 24 | -------------------------------------------------------------------------------- /binder/README.md: -------------------------------------------------------------------------------- 1 | # foundry-binder 2 | 3 | Utilities for generating bindings for solidity projects in one step. 4 | 5 | First add `foundry-binder` to your cargo build-dependencies. 6 | 7 | ```toml 8 | [build-dependencies] 9 | foundry-binder = { git = "https://github.com/gakonst/foundry" } 10 | # required in order to enable ssh support in [libgit2](https://github.com/rust-lang/git2-rs) 11 | git2 = "0.13" 12 | ``` 13 | 14 | ```rust 15 | use foundry_binder::{Binder, RepositoryBuilder, Url}; 16 | 17 | // github repository url 18 | const REPO_URL: &str = ""; 19 | 20 | // the release tag for which to generate bindings for 21 | const RELEASE_TAG: &str = "v3.0.0"; 22 | 23 | /// This clones the project, builds the project and generates rust bindings 24 | fn generate() { 25 | let binder = 26 | Binder::new(RepositoryBuilder::new(Url::parse(REPO_URL).unwrap()) 27 | // generate bindings for this release tag 28 | // if not set, then the default branch will be used 29 | .tag(RELEASE_TAG)) 30 | // keep build artifacts in `artifacts` folder 31 | .keep_artifacts("artifacts"); 32 | 33 | binder.generate().expect("Failed to generate bindings") 34 | } 35 | 36 | fn main() { 37 | // only generate if `FRESH_BINDINGS` env var is set 38 | if std::env::var("FRESH_BINDINGS").is_ok() { 39 | generate() 40 | } 41 | } 42 | ``` -------------------------------------------------------------------------------- /cast/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cast" 3 | version = "0.2.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | readme = "README.md" 7 | repository = "https://github.com/gakonst/foundry" 8 | keywords = ["ethereum", "web3"] 9 | 10 | 11 | [dependencies] 12 | foundry-utils = { path = "../utils" } 13 | futures = "0.3.17" 14 | ethers-etherscan = { git = "https://github.com/gakonst/ethers-rs", default-features = false } 15 | ethers-core = { git = "https://github.com/gakonst/ethers-rs", default-features = false } 16 | ethers-providers = { git = "https://github.com/gakonst/ethers-rs", default-features = false } 17 | ethers-signers = { git = "https://github.com/gakonst/ethers-rs", default-features = false } 18 | eyre = "0.6.5" 19 | rustc-hex = "2.1.0" 20 | serde_json = "1.0.67" 21 | chrono = "0.2" 22 | hex = "0.4.3" 23 | 24 | [dev-dependencies] 25 | async-trait = "0.1.53" 26 | serde = "1.0.136" 27 | tokio = "1.17.0" 28 | thiserror = "1.0.30" 29 | 30 | [features] 31 | default = ["ledger", "trezor"] 32 | ledger = ["ethers-signers/ledger"] 33 | trezor = ["ethers-signers/trezor"] 34 | -------------------------------------------------------------------------------- /cast/README.md: -------------------------------------------------------------------------------- 1 | # `cast` 2 | 3 | **Need help with Cast? Read the [📖 Foundry Book (Cast Guide)][foundry-book-cast-guide] (WIP)!** 4 | 5 | [foundry-book-cast-guide]: https://book.getfoundry.sh/cast/index.html 6 | 7 | ## Features 8 | 9 | - [x] `--abi-decode` 10 | - [x] `--calldata-decode` 11 | - [x] `--from-ascii` (with `--from-utf8` alias) 12 | - [x] `--from-bin` 13 | - [x] `--from-fix` 14 | - [x] `--from-wei` 15 | - [x] `--max-int` 16 | - [x] `--max-uint` 17 | - [x] `--min-int` 18 | - [x] `--to-checksum-address` (`--to-address` in dapptools) 19 | - [x] `--to-ascii` 20 | - [x] `--to-bytes32` 21 | - [x] `--to-dec` 22 | - [x] `--to-fix` 23 | - [x] `--to-hex` 24 | - [x] `--to-hexdata` 25 | - [x] `--to-int256` 26 | - [x] `--to-uint256` 27 | - [x] `--to-wei` 28 | - [x] `4byte` 29 | - [x] `4byte-decode` 30 | - [x] `4byte-event` 31 | - [x] `abi-encode` 32 | - [x] `age` 33 | - [x] `balance` 34 | - [x] `basefee` 35 | - [x] `block` 36 | - [x] `block-number` 37 | - [ ] `bundle-source` 38 | - [x] `call` (partial) 39 | - [x] `calldata` 40 | - [x] `chain` 41 | - [x] `chain-id` 42 | - [x] `client` 43 | - [x] `code` 44 | - [ ] `debug` 45 | - [x] `estimate` 46 | - [x] `etherscan-source` 47 | - [ ] `events` 48 | - [x] `gas-price` 49 | - [x] `index` 50 | - [x] `keccak` 51 | - [ ] `logs` 52 | - [x] `lookup-address` 53 | - [ ] `ls` 54 | - [ ] `mktx` 55 | - [x] `namehash` 56 | - [x] `nonce` 57 | - [x] `publish` 58 | - [x] `receipt` 59 | - [x] `resolve-name` 60 | - [ ] `run-tx` 61 | - [x] `send` (partial) 62 | - [x] `sig` 63 | - [ ] `sign` 64 | - [x] `storage` 65 | - [x] `tx` 66 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foundry-cli" 3 | version = "0.2.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | readme = "README.md" 7 | repository = "https://github.com/gakonst/foundry" 8 | keywords = ["ethereum", "web3"] 9 | 10 | [build-dependencies] 11 | vergen = { version = "6.0.0", default-features = false, features = [ 12 | "build", 13 | "rustc", 14 | "git", 15 | ] } 16 | 17 | [dependencies] 18 | # foundry internal 19 | forge-fmt = { path = "../fmt" } 20 | foundry-utils = { path = "../utils" } 21 | forge = { path = "../forge" } 22 | foundry-config = { path = "../config" } 23 | foundry-common = { path = "../common" } 24 | cast = { path = "../cast" } 25 | ui = { path = "../ui" } 26 | 27 | # eth 28 | ethers = { git = "https://github.com/gakonst/ethers-rs", default-features = false } 29 | solang-parser = "0.1.11" 30 | 31 | # cli 32 | clap = { version = "3.0.10", features = [ 33 | "derive", 34 | "env", 35 | "unicode", 36 | "wrap_help", 37 | ] } 38 | clap_complete = "3.0.4" 39 | ansi_term = "0.12.1" 40 | tracing-error = "0.2.0" 41 | tracing-subscriber = { version = "0.3", features = ["registry", "env-filter", "fmt"] } 42 | tracing = "0.1.26" 43 | console = "0.15.0" 44 | watchexec = "2.0.0-pre.11" 45 | atty = "0.2.14" 46 | comfy-table = "5.0.0" 47 | xshell = "0.2.1" 48 | 49 | # async / parallel 50 | tokio = { version = "1.11.0", features = ["macros"] } 51 | futures = "0.3.17" 52 | rayon = "1.5.1" 53 | 54 | # disk / paths 55 | walkdir = "2.3.2" 56 | dunce = "1.0.2" 57 | glob = "0.3.0" 58 | globset = "0.4.8" 59 | 60 | # misc 61 | eyre = "0.6" 62 | color-eyre = "0.6" 63 | rustc-hex = "2.1.0" 64 | serde_json = "1.0.67" 65 | regex = { version = "1.5.4", default-features = false } 66 | rpassword = "5.0.1" 67 | hex = "0.4.3" 68 | serde = "1.0.133" 69 | proptest = "1.0.0" 70 | semver = "1.0.5" 71 | once_cell = "1.9.0" 72 | similar = { version = "2.1.0", features = ["inline"] } 73 | toml = "0.5" 74 | 75 | [dev-dependencies] 76 | foundry-utils = { path = "./../utils", features = ["test"] } 77 | foundry-cli-test-utils = { path = "./test-utils" } 78 | pretty_assertions = "1.0.0" 79 | 80 | [features] 81 | default = ["rustls"] 82 | solc-asm = ["ethers/solc-sha2-asm"] 83 | rustls = ["ethers/rustls"] 84 | openssl = ["ethers/openssl"] 85 | 86 | # feature for integration tests that test external projects 87 | external-integration-tests = [] 88 | 89 | [[bin]] 90 | name = "cast" 91 | path = "src/cast.rs" 92 | doc = false 93 | 94 | [[bin]] 95 | name = "forge" 96 | path = "src/forge.rs" 97 | doc = false 98 | -------------------------------------------------------------------------------- /cli/assets/.gitignoreTemplate: -------------------------------------------------------------------------------- 1 | cache/ 2 | out/ 3 | -------------------------------------------------------------------------------- /cli/assets/ContractTemplate.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | contract Contract {} 5 | -------------------------------------------------------------------------------- /cli/assets/ContractTemplate.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "ds-test/test.sol"; 5 | 6 | contract ContractTest is DSTest { 7 | function setUp() public {} 8 | 9 | function testExample() public { 10 | assertTrue(true); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /cli/assets/workflowTemplate.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /cli/build.rs: -------------------------------------------------------------------------------- 1 | use vergen::{Config, ShaKind}; 2 | 3 | fn main() { 4 | let mut config = Config::default(); 5 | // Change the SHA output to the short variant 6 | *config.git_mut().sha_kind_mut() = ShaKind::Short; 7 | vergen::vergen(config) 8 | .unwrap_or_else(|e| panic!("vergen crate failed to generate version information! {e}")); 9 | } 10 | -------------------------------------------------------------------------------- /cli/src/cmd/cast/find_block.rs: -------------------------------------------------------------------------------- 1 | //! cast find-block subcommand 2 | 3 | use crate::{cmd::Cmd, utils::consume_config_rpc_url}; 4 | use cast::Cast; 5 | use clap::Parser; 6 | use ethers::prelude::*; 7 | use eyre::Result; 8 | use futures::{future::BoxFuture, join}; 9 | 10 | #[derive(Debug, Clone, Parser)] 11 | pub struct FindBlockArgs { 12 | #[clap(help = "The UNIX timestamp to search for (in seconds)")] 13 | timestamp: u64, 14 | #[clap(long, env = "ETH_RPC_URL")] 15 | rpc_url: Option, 16 | } 17 | 18 | impl Cmd for FindBlockArgs { 19 | type Output = BoxFuture<'static, Result<()>>; 20 | 21 | fn run(self) -> Result { 22 | let FindBlockArgs { timestamp, rpc_url } = self; 23 | Ok(Box::pin(Self::query_block(timestamp, rpc_url))) 24 | } 25 | } 26 | 27 | impl FindBlockArgs { 28 | async fn query_block(timestamp: u64, rpc_url: Option) -> Result<()> { 29 | let ts_target = U256::from(timestamp); 30 | let rpc_url = consume_config_rpc_url(rpc_url); 31 | 32 | let provider = Provider::try_from(rpc_url)?; 33 | let last_block_num = provider.get_block_number().await?; 34 | let cast_provider = Cast::new(provider); 35 | 36 | let res = join!(cast_provider.timestamp(last_block_num), cast_provider.timestamp(1)); 37 | let ts_block_latest = res.0?; 38 | let ts_block_1 = res.1?; 39 | 40 | let block_num = if ts_block_latest.lt(&ts_target) { 41 | // If the most recent block's timestamp is below the target, return it 42 | last_block_num 43 | } else if ts_block_1.gt(&ts_target) { 44 | // If the target timestamp is below block 1's timestamp, return that 45 | U64::from(1_u64) 46 | } else { 47 | // Otherwise, find the block that is closest to the timestamp 48 | let mut low_block = U64::from(1_u64); // block 0 has a timestamp of 0: https://github.com/ethereum/go-ethereum/issues/17042#issuecomment-559414137 49 | let mut high_block = last_block_num; 50 | let mut matching_block: Option = None; 51 | while high_block.gt(&low_block) && matching_block.is_none() { 52 | // Get timestamp of middle block (this approach approach to avoids overflow) 53 | let high_minus_low_over_2 = high_block 54 | .checked_sub(low_block) 55 | .ok_or_else(|| eyre::eyre!("unexpected underflow")) 56 | .unwrap() 57 | .checked_div(U64::from(2_u64)) 58 | .unwrap(); 59 | let mid_block = high_block.checked_sub(high_minus_low_over_2).unwrap(); 60 | let ts_mid_block = cast_provider.timestamp(mid_block).await?; 61 | 62 | // Check if we've found a match or should keep searching 63 | if ts_mid_block.eq(&ts_target) { 64 | matching_block = Some(mid_block) 65 | } else if high_block.checked_sub(low_block).unwrap().eq(&U64::from(1_u64)) { 66 | // The target timestamp is in between these blocks. This rounds to the 67 | // highest block if timestamp is equidistant between blocks 68 | let res = join!( 69 | cast_provider.timestamp(high_block), 70 | cast_provider.timestamp(low_block) 71 | ); 72 | let ts_high = res.0.unwrap(); 73 | let ts_low = res.1.unwrap(); 74 | let high_diff = ts_high.checked_sub(ts_target).unwrap(); 75 | let low_diff = ts_target.checked_sub(ts_low).unwrap(); 76 | let is_low = low_diff.lt(&high_diff); 77 | matching_block = if is_low { Some(low_block) } else { Some(high_block) } 78 | } else if ts_mid_block.lt(&ts_target) { 79 | low_block = mid_block; 80 | } else { 81 | high_block = mid_block; 82 | } 83 | } 84 | matching_block.unwrap_or(low_block) 85 | }; 86 | println!("{block_num}"); 87 | 88 | Ok(()) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /cli/src/cmd/cast/mod.rs: -------------------------------------------------------------------------------- 1 | //! Subcommands for cast 2 | //! 3 | //! All subcommands should respect the `foundry_config::Config`. 4 | //! If a subcommand accepts values that are supported by the `Config`, then the subcommand should 5 | //! implement `figment::Provider` which allows the subcommand to override the config's defaults, see 6 | //! [`foundry_config::Config`]. 7 | 8 | pub mod find_block; 9 | -------------------------------------------------------------------------------- /cli/src/cmd/forge/bind.rs: -------------------------------------------------------------------------------- 1 | use crate::cmd::utils::Cmd; 2 | 3 | use clap::{Parser, ValueHint}; 4 | use ethers::contract::MultiAbigen; 5 | use foundry_config::{ 6 | figment::{ 7 | self, 8 | error::Kind::InvalidType, 9 | value::{Dict, Map, Value}, 10 | Metadata, Profile, Provider, 11 | }, 12 | impl_figment_convert, Config, 13 | }; 14 | use serde::Serialize; 15 | use std::{fs, path::PathBuf}; 16 | 17 | impl_figment_convert!(BindArgs); 18 | 19 | static DEFAULT_CRATE_NAME: &str = "foundry-contracts"; 20 | static DEFAULT_CRATE_VERSION: &str = "0.0.1"; 21 | 22 | #[derive(Debug, Clone, Parser, Serialize)] 23 | pub struct BindArgs { 24 | #[clap( 25 | help = "The project's root path. By default, this is the root directory of the current Git repository or the current working directory if it is not part of a Git repository", 26 | long, 27 | value_hint = ValueHint::DirPath 28 | )] 29 | #[serde(skip)] 30 | pub root: Option, 31 | 32 | #[clap( 33 | help = "Path to where the contract artifacts are stored", 34 | long = "bindings-path", 35 | short, 36 | value_hint = ValueHint::DirPath 37 | )] 38 | #[serde(skip)] 39 | pub bindings: Option, 40 | 41 | #[clap( 42 | long = "crate-name", 43 | help = "The name of the Rust crate to generate. This should be a valid crates.io crate name. However, it is not currently validated by this command.", 44 | default_value = DEFAULT_CRATE_NAME, 45 | )] 46 | #[serde(skip)] 47 | crate_name: String, 48 | 49 | #[clap( 50 | long = "crate-version", 51 | help = "The version of the Rust crate to generate. This should be a standard semver version string. However, it is not currently validated by this command.", 52 | default_value = DEFAULT_CRATE_VERSION, 53 | )] 54 | #[serde(skip)] 55 | crate_version: String, 56 | 57 | #[clap(long, help = "Generate the bindings as a module instead of a crate")] 58 | module: bool, 59 | 60 | #[clap( 61 | long = "overwrite", 62 | help = "Overwrite existing generated bindings. By default, the command will check that the bindings are correct, and then exit. If --overwrite is passed, it will instead delete and overwrite the bindings." 63 | )] 64 | #[serde(skip)] 65 | overwrite: bool, 66 | 67 | #[clap(long = "single-file", help = "Generate bindings as a single file.")] 68 | #[serde(skip)] 69 | single_file: bool, 70 | } 71 | 72 | impl BindArgs { 73 | /// Get the path to the foundry artifacts directory 74 | fn artifacts(&self) -> PathBuf { 75 | let c: Config = self.into(); 76 | c.out 77 | } 78 | 79 | /// Get the path to the root of the autogenerated crate 80 | fn bindings_root(&self) -> PathBuf { 81 | self.bindings.clone().unwrap_or_else(|| self.artifacts().join("bindings")) 82 | } 83 | 84 | /// `true` if the bindings root already exists 85 | fn bindings_exist(&self) -> bool { 86 | self.bindings_root().is_dir() 87 | } 88 | 89 | /// Instantiate the multi-abigen 90 | fn get_multi(&self) -> eyre::Result { 91 | let multi = MultiAbigen::from_json_files(self.artifacts())?; 92 | 93 | eyre::ensure!( 94 | !multi.is_empty(), 95 | r#" 96 | No contract artifacts found. Hint: Have you built your contracts yet? `forge bind` does not currently invoke `forge build`, although this is planned for future versions. 97 | "# 98 | ); 99 | Ok(multi) 100 | } 101 | 102 | /// Check that the existing bindings match the expected abigen output 103 | fn check_existing_bindings(&self) -> eyre::Result<()> { 104 | let bindings = self.get_multi()?.build()?; 105 | println!("Checking bindings for {} contracts.", bindings.len()); 106 | if !self.module { 107 | bindings.ensure_consistent_crate( 108 | &self.crate_name, 109 | &self.crate_version, 110 | self.bindings_root(), 111 | self.single_file, 112 | )?; 113 | } else { 114 | bindings.ensure_consistent_module(self.bindings_root(), self.single_file)?; 115 | } 116 | println!("OK."); 117 | Ok(()) 118 | } 119 | 120 | /// Generate the bindings 121 | fn generate_bindings(&self) -> eyre::Result<()> { 122 | let bindings = self.get_multi()?.build()?; 123 | println!("Generating bindings for {} contracts", bindings.len()); 124 | if !self.module { 125 | bindings.write_to_crate( 126 | &self.crate_name, 127 | &self.crate_version, 128 | self.bindings_root(), 129 | self.single_file, 130 | )?; 131 | } else { 132 | bindings.write_to_module(self.bindings_root(), self.single_file)?; 133 | } 134 | Ok(()) 135 | } 136 | } 137 | 138 | impl Cmd for BindArgs { 139 | type Output = (); 140 | 141 | fn run(self) -> eyre::Result { 142 | if !self.overwrite && self.bindings_exist() { 143 | println!("Bindings found. Checking for consistency."); 144 | return self.check_existing_bindings() 145 | } 146 | 147 | if self.overwrite { 148 | fs::remove_dir_all(self.bindings_root())?; 149 | } 150 | 151 | self.generate_bindings()?; 152 | 153 | println!("Bindings have been output to {}", self.bindings_root().to_str().unwrap()); 154 | Ok(()) 155 | } 156 | } 157 | 158 | // Make this args a `figment::Provider` so that it can be merged into the `Config` 159 | impl Provider for BindArgs { 160 | fn metadata(&self) -> Metadata { 161 | Metadata::named("Bind Args Provider") 162 | } 163 | 164 | fn data(&self) -> Result, figment::Error> { 165 | let value = Value::serialize(self)?; 166 | let error = InvalidType(value.to_actual(), "map".into()); 167 | let dict = value.into_dict().ok_or(error)?; 168 | Ok(Map::from([(Config::selected_profile(), dict)])) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /cli/src/cmd/forge/config.rs: -------------------------------------------------------------------------------- 1 | //! config command 2 | 3 | use crate::cmd::{forge::build::BuildArgs, utils::Cmd}; 4 | use clap::Parser; 5 | use foundry_common::evm::EvmArgs; 6 | use foundry_config::{figment::Figment, Config}; 7 | 8 | foundry_config::impl_figment_convert!(ConfigArgs, opts, evm_opts); 9 | 10 | /// Command to list currently set config values 11 | #[derive(Debug, Clone, Parser)] 12 | pub struct ConfigArgs { 13 | #[clap(help = "prints currently set config values as json", long)] 14 | json: bool, 15 | #[clap(help = "prints basic set of currently set config values", long)] 16 | basic: bool, 17 | // support nested build arguments 18 | #[clap(flatten)] 19 | opts: BuildArgs, 20 | #[clap(flatten)] 21 | evm_opts: EvmArgs, 22 | } 23 | 24 | impl Cmd for ConfigArgs { 25 | type Output = (); 26 | 27 | fn run(self) -> eyre::Result { 28 | let figment: Figment = From::from(&self); 29 | let config = Config::from_provider(figment); 30 | let s = if self.basic { 31 | let config = config.into_basic(); 32 | if self.json { 33 | serde_json::to_string_pretty(&config)? 34 | } else { 35 | config.to_string_pretty()? 36 | } 37 | } else if self.json { 38 | serde_json::to_string_pretty(&config)? 39 | } else { 40 | config.to_string_pretty()? 41 | }; 42 | 43 | println!("{s}"); 44 | Ok(()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /cli/src/cmd/forge/flatten.rs: -------------------------------------------------------------------------------- 1 | use super::build::{CoreBuildArgs, ProjectPathsArgs}; 2 | use crate::cmd::Cmd; 3 | use clap::{Parser, ValueHint}; 4 | use foundry_config::Config; 5 | use std::path::PathBuf; 6 | 7 | #[derive(Debug, Clone, Parser)] 8 | pub struct FlattenArgs { 9 | #[clap(help = "The path to the contract to flatten.", value_hint = ValueHint::FilePath)] 10 | pub target_path: PathBuf, 11 | 12 | #[clap( 13 | long, 14 | short, 15 | help = "The path to output the flattened contract.", 16 | long_help = "The path to output the flattened contract. If not specified, the flattened contract will be output to stdout.", 17 | value_hint = ValueHint::FilePath 18 | )] 19 | pub output: Option, 20 | 21 | #[clap(flatten, next_help_heading = "PROJECT OPTIONS")] 22 | project_paths: ProjectPathsArgs, 23 | } 24 | 25 | impl Cmd for FlattenArgs { 26 | type Output = (); 27 | fn run(self) -> eyre::Result { 28 | let FlattenArgs { target_path, output, project_paths } = self; 29 | 30 | // flatten is a subset of `BuildArgs` so we can reuse that to get the config 31 | let build_args = CoreBuildArgs { 32 | project_paths, 33 | out_path: Default::default(), 34 | compiler: Default::default(), 35 | ignored_error_codes: vec![], 36 | no_auto_detect: false, 37 | use_solc: None, 38 | offline: false, 39 | force: false, 40 | libraries: vec![], 41 | via_ir: false, 42 | config_path: None, 43 | }; 44 | 45 | let config = Config::from(&build_args); 46 | 47 | let paths = config.project_paths(); 48 | let target_path = dunce::canonicalize(target_path)?; 49 | let flattened = paths 50 | .flatten(&target_path) 51 | .map_err(|err| eyre::Error::msg(format!("Failed to flatten the file: {err}")))?; 52 | 53 | match output { 54 | Some(output) => { 55 | std::fs::create_dir_all(&output.parent().unwrap())?; 56 | std::fs::write(&output, flattened)?; 57 | println!("Flattened file written at {}", output.display()); 58 | } 59 | None => println!("{flattened}"), 60 | }; 61 | 62 | Ok(()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /cli/src/cmd/forge/mod.rs: -------------------------------------------------------------------------------- 1 | //! Subcommands for forge 2 | //! 3 | //! All subcommands should respect the `foundry_config::Config`. 4 | //! If a subcommand accepts values that are supported by the `Config`, then the subcommand should 5 | //! implement `figment::Provider` which allows the subcommand to override the config's defaults, see 6 | //! [`foundry_config::Config`]. 7 | //! 8 | //! See [`BuildArgs`] for a reference implementation. 9 | //! And [`RunArgs`] for how to merge `Providers`. 10 | //! 11 | //! # Example 12 | //! 13 | //! create a `clap` subcommand into a `figment::Provider` and integrate it in the 14 | //! `foundry_config::Config`: 15 | //! 16 | //! ```rust 17 | //! use crate::{cmd::build::BuildArgs, foundry_common::evm::EvmArgs}; 18 | //! use clap::Parser; 19 | //! use foundry_config::{figment::Figment, *}; 20 | //! 21 | //! // A new clap subcommand that accepts both `EvmArgs` and `BuildArgs` 22 | //! #[derive(Debug, Clone, Parser)] 23 | //! pub struct MyArgs { 24 | //! #[clap(flatten)] 25 | //! evm_opts: EvmArgs, 26 | //! #[clap(flatten)] 27 | //! opts: BuildArgs, 28 | //! } 29 | //! 30 | //! // add `Figment` and `Config` converters 31 | //! foundry_config::impl_figment_convert!(MyArgs, opts, evm_opts); 32 | //! let args = MyArgs::parse_from(["build"]); 33 | //! 34 | //! let figment: Figment = From::from(&args); 35 | //! let evm_opts = figment.extract::().unwrap(); 36 | //! 37 | //! let config: Config = From::from(&args); 38 | //! ``` 39 | 40 | pub mod bind; 41 | pub mod build; 42 | pub mod certora; 43 | pub mod config; 44 | pub mod create; 45 | pub mod flatten; 46 | pub mod fmt; 47 | pub mod init; 48 | pub mod inspect; 49 | pub mod install; 50 | pub mod remappings; 51 | pub mod run; 52 | pub mod snapshot; 53 | pub mod test; 54 | pub mod tree; 55 | pub mod verify; 56 | pub mod watch; 57 | -------------------------------------------------------------------------------- /cli/src/cmd/forge/remappings.rs: -------------------------------------------------------------------------------- 1 | //! remappings command 2 | 3 | use crate::cmd::Cmd; 4 | use clap::{Parser, ValueHint}; 5 | use ethers::solc::{remappings::Remapping, ProjectPathsConfig}; 6 | use std::path::{Path, PathBuf}; 7 | 8 | /// Command to list remappings 9 | #[derive(Debug, Clone, Parser)] 10 | pub struct RemappingArgs { 11 | #[clap( 12 | help = "The project's root path. Defaults to the current working directory.", 13 | long, 14 | value_hint = ValueHint::DirPath 15 | )] 16 | root: Option, 17 | #[clap( 18 | help = "The path to the library folder.", 19 | long, 20 | value_hint = ValueHint::DirPath 21 | )] 22 | lib_path: Vec, 23 | } 24 | 25 | impl Cmd for RemappingArgs { 26 | type Output = (); 27 | 28 | fn run(self) -> eyre::Result { 29 | let root = self.root.unwrap_or_else(|| std::env::current_dir().unwrap()); 30 | let root = dunce::canonicalize(root)?; 31 | 32 | let lib_path = if self.lib_path.is_empty() { 33 | ProjectPathsConfig::find_libs(&root) 34 | } else { 35 | self.lib_path 36 | }; 37 | let remappings: Vec<_> = 38 | lib_path.iter().flat_map(|lib| relative_remappings(lib, &root)).collect(); 39 | remappings.iter().for_each(|x| println!("{x}")); 40 | Ok(()) 41 | } 42 | } 43 | 44 | /// Returns all remappings found in the `lib` path relative to `root` 45 | pub fn relative_remappings(lib: &Path, root: &Path) -> Vec { 46 | Remapping::find_many(lib) 47 | .into_iter() 48 | .map(|r| r.into_relative(root).to_relative_remapping()) 49 | .map(Into::into) 50 | .collect() 51 | } 52 | -------------------------------------------------------------------------------- /cli/src/cmd/forge/tree.rs: -------------------------------------------------------------------------------- 1 | //! tree command 2 | 3 | use crate::cmd::{forge::build::ProjectPathsArgs, Cmd}; 4 | use clap::Parser; 5 | use ethers::solc::Graph; 6 | use foundry_config::Config; 7 | 8 | foundry_config::impl_figment_convert!(TreeArgs, opts); 9 | use ethers::solc::resolver::{Charset, TreeOptions}; 10 | 11 | /// Command to display the project's dependency tree 12 | #[derive(Debug, Clone, Parser)] 13 | pub struct TreeArgs { 14 | #[clap(help = "Do not de-duplicate (repeats all shared dependencies)", long)] 15 | no_dedupe: bool, 16 | #[clap(help = "Character set to use in output: utf8, ascii", default_value = "utf8", long)] 17 | charset: Charset, 18 | #[clap(flatten, next_help_heading = "PROJECT OPTIONS")] 19 | opts: ProjectPathsArgs, 20 | } 21 | 22 | impl Cmd for TreeArgs { 23 | type Output = (); 24 | 25 | fn run(self) -> eyre::Result { 26 | let config: Config = From::from(&self); 27 | let graph = Graph::resolve(&config.project_paths())?; 28 | let opts = TreeOptions { charset: self.charset, no_dedupe: self.no_dedupe }; 29 | graph.print_with_options(opts); 30 | 31 | Ok(()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cli/src/cmd/mod.rs: -------------------------------------------------------------------------------- 1 | //! Subcommands 2 | //! 3 | //! All subcommands should respect the `foundry_config::Config`. 4 | //! If a subcommand accepts values that are supported by the `Config`, then the subcommand should 5 | //! implement `figment::Provider` which allows the subcommand to override the config's defaults, see 6 | //! [`foundry_config::Config`]. 7 | 8 | pub mod cast; 9 | pub mod forge; 10 | 11 | // Re-export our shared utilities 12 | mod utils; 13 | pub use utils::*; 14 | -------------------------------------------------------------------------------- /cli/src/cmd/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::opts::forge::ContractInfo; 2 | use ethers::{ 3 | abi::Abi, 4 | prelude::artifacts::{CompactBytecode, CompactDeployedBytecode}, 5 | solc::cache::SolFilesCache, 6 | }; 7 | use std::path::PathBuf; 8 | 9 | /// Common trait for all cli commands 10 | pub trait Cmd: clap::Parser + Sized { 11 | type Output; 12 | fn run(self) -> eyre::Result; 13 | } 14 | 15 | use ethers::solc::{artifacts::CompactContractBytecode, Project, ProjectCompileOutput}; 16 | 17 | /// Given a project and its compiled artifacts, proceeds to return the ABI, Bytecode and 18 | /// Runtime Bytecode of the given contract. 19 | pub fn read_artifact( 20 | project: &Project, 21 | compiled: ProjectCompileOutput, 22 | contract: ContractInfo, 23 | ) -> eyre::Result<(Abi, CompactBytecode, CompactDeployedBytecode)> { 24 | Ok(match contract.path { 25 | Some(path) => get_artifact_from_path(project, path, contract.name)?, 26 | None => get_artifact_from_name(contract, compiled)?, 27 | }) 28 | } 29 | 30 | /// Helper function for finding a contract by ContractName 31 | // TODO: Is there a better / more ergonomic way to get the artifacts given a project and a 32 | // contract name? 33 | fn get_artifact_from_name( 34 | contract: ContractInfo, 35 | compiled: ProjectCompileOutput, 36 | ) -> eyre::Result<(Abi, CompactBytecode, CompactDeployedBytecode)> { 37 | let mut has_found_contract = false; 38 | let mut contract_artifact = None; 39 | 40 | for (artifact_id, artifact) in compiled.into_artifacts() { 41 | if artifact_id.name == contract.name { 42 | if has_found_contract { 43 | eyre::bail!("contract with duplicate name. pass path") 44 | } 45 | has_found_contract = true; 46 | contract_artifact = Some(artifact); 47 | } 48 | } 49 | 50 | Ok(match contract_artifact { 51 | Some(artifact) => ( 52 | artifact 53 | .abi 54 | .map(Into::into) 55 | .ok_or_else(|| eyre::Error::msg(format!("abi not found for {}", contract.name)))?, 56 | artifact.bytecode.ok_or_else(|| { 57 | eyre::Error::msg(format!("bytecode not found for {}", contract.name)) 58 | })?, 59 | artifact.deployed_bytecode.ok_or_else(|| { 60 | eyre::Error::msg(format!("bytecode not found for {}", contract.name)) 61 | })?, 62 | ), 63 | None => { 64 | eyre::bail!("could not find artifact") 65 | } 66 | }) 67 | } 68 | 69 | /// Find using src/ContractSource.sol:ContractName 70 | fn get_artifact_from_path( 71 | project: &Project, 72 | contract_path: String, 73 | contract_name: String, 74 | ) -> eyre::Result<(Abi, CompactBytecode, CompactDeployedBytecode)> { 75 | // Get sources from the requested location 76 | let abs_path = dunce::canonicalize(PathBuf::from(contract_path))?; 77 | 78 | let cache = SolFilesCache::read_joined(&project.paths)?; 79 | 80 | // Read the artifact from disk 81 | let artifact: CompactContractBytecode = cache.read_artifact(abs_path, &contract_name)?; 82 | 83 | Ok(( 84 | artifact 85 | .abi 86 | .ok_or_else(|| eyre::Error::msg(format!("abi not found for {contract_name}")))?, 87 | artifact 88 | .bytecode 89 | .ok_or_else(|| eyre::Error::msg(format!("bytecode not found for {contract_name}")))?, 90 | artifact 91 | .deployed_bytecode 92 | .ok_or_else(|| eyre::Error::msg(format!("bytecode not found for {contract_name}")))?, 93 | )) 94 | } 95 | -------------------------------------------------------------------------------- /cli/src/forge.rs: -------------------------------------------------------------------------------- 1 | pub mod cmd; 2 | pub mod compile; 3 | mod opts; 4 | mod term; 5 | mod utils; 6 | 7 | use crate::cmd::{forge::watch, Cmd}; 8 | use opts::forge::{Dependency, Opts, Subcommands}; 9 | use std::process::Command; 10 | 11 | use clap::{IntoApp, Parser}; 12 | use clap_complete::generate; 13 | 14 | fn main() -> eyre::Result<()> { 15 | color_eyre::install()?; 16 | utils::subscriber(); 17 | 18 | let opts = Opts::parse(); 19 | match opts.sub { 20 | Subcommands::Test(cmd) => { 21 | if cmd.is_watch() { 22 | utils::block_on(watch::watch_test(cmd))?; 23 | } else { 24 | let outcome = cmd.run()?; 25 | outcome.ensure_ok()?; 26 | } 27 | } 28 | Subcommands::Bind(cmd) => { 29 | cmd.run()?; 30 | } 31 | Subcommands::Build(cmd) => { 32 | if cmd.is_watch() { 33 | utils::block_on(crate::cmd::forge::watch::watch_build(cmd))?; 34 | } else { 35 | cmd.run()?; 36 | } 37 | } 38 | Subcommands::Run(cmd) => { 39 | cmd.run()?; 40 | } 41 | Subcommands::VerifyContract(args) => { 42 | utils::block_on(args.run())?; 43 | } 44 | Subcommands::VerifyCheck(args) => { 45 | utils::block_on(args.run())?; 46 | } 47 | Subcommands::Create(cmd) => { 48 | cmd.run()?; 49 | } 50 | Subcommands::Update { lib } => { 51 | let mut cmd = Command::new("git"); 52 | 53 | cmd.args(&["submodule", "update", "--remote", "--init", "--recursive"]); 54 | 55 | // if a lib is specified, open it 56 | if let Some(lib) = lib { 57 | cmd.args(&["--", lib.display().to_string().as_str()]); 58 | } 59 | 60 | cmd.spawn()?.wait()?; 61 | } 62 | // TODO: Make it work with updates? 63 | Subcommands::Install(cmd) => { 64 | cmd.run()?; 65 | } 66 | Subcommands::Remove { dependencies } => { 67 | remove(std::env::current_dir()?, dependencies)?; 68 | } 69 | Subcommands::Remappings(cmd) => { 70 | cmd.run()?; 71 | } 72 | Subcommands::Init(cmd) => { 73 | cmd.run()?; 74 | } 75 | Subcommands::Completions { shell } => { 76 | generate(shell, &mut Opts::command(), "forge", &mut std::io::stdout()) 77 | } 78 | Subcommands::Clean { root } => { 79 | let config = utils::load_config_with_root(root); 80 | config.project()?.cleanup()?; 81 | } 82 | Subcommands::Snapshot(cmd) => { 83 | if cmd.is_watch() { 84 | utils::block_on(crate::cmd::forge::watch::watch_snapshot(cmd))?; 85 | } else { 86 | cmd.run()?; 87 | } 88 | } 89 | // Subcommands::Fmt(cmd) => { 90 | // cmd.run()?; 91 | // } 92 | Subcommands::Config(cmd) => { 93 | cmd.run()?; 94 | } 95 | Subcommands::Flatten(cmd) => { 96 | cmd.run()?; 97 | } 98 | Subcommands::Inspect(cmd) => { 99 | cmd.run()?; 100 | } 101 | Subcommands::Tree(cmd) => { 102 | cmd.run()?; 103 | } 104 | Subcommands::Certora(cmd) => { 105 | cmd.run()?; 106 | } 107 | } 108 | 109 | Ok(()) 110 | } 111 | 112 | fn remove(root: impl AsRef, dependencies: Vec) -> eyre::Result<()> { 113 | let libs = std::path::Path::new("lib"); 114 | let git_mod_libs = std::path::Path::new(".git/modules/lib"); 115 | 116 | dependencies.iter().try_for_each(|dep| -> eyre::Result<_> { 117 | let target_dir = if let Some(alias) = &dep.alias { alias } else { &dep.name }; 118 | let path = libs.join(&target_dir); 119 | let git_mod_path = git_mod_libs.join(&target_dir); 120 | println!("Removing {} in {:?}, (url: {}, tag: {:?})", dep.name, path, dep.url, dep.tag); 121 | 122 | // remove submodule entry from .git/config 123 | Command::new("git") 124 | .args(&["submodule", "deinit", "-f", &path.display().to_string()]) 125 | .current_dir(&root) 126 | .spawn()? 127 | .wait()?; 128 | 129 | // remove the submodule repository from .git/modules directory 130 | Command::new("rm") 131 | .args(&["-rf", &git_mod_path.display().to_string()]) 132 | .current_dir(&root) 133 | .spawn()? 134 | .wait()?; 135 | 136 | // remove the leftover submodule directory 137 | Command::new("git") 138 | .args(&["rm", "-f", &path.display().to_string()]) 139 | .current_dir(&root) 140 | .spawn()? 141 | .wait()?; 142 | 143 | Ok(()) 144 | }) 145 | } 146 | -------------------------------------------------------------------------------- /cli/test-utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foundry-cli-test-utils" 3 | version = "0.1.0" 4 | description = """ 5 | foundry cli test utilities 6 | """ 7 | edition = "2021" 8 | license = "MIT OR Apache-2.0" 9 | repository = "https://github.com/gakonst/foundry" 10 | 11 | [dependencies] 12 | tempfile = "3.2.0" 13 | ethers-solc = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["project-util"] } 14 | walkdir = "2.3.2" 15 | once_cell = "1.9.0" 16 | foundry-config = { path = "../../config" } 17 | serde_json = "1.0.67" 18 | parking_lot = "0.12.0" 19 | eyre = "0.6" 20 | -------------------------------------------------------------------------------- /cli/test-utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate core; 2 | 3 | // Macros useful for testing. 4 | mod macros; 5 | 6 | // Utilities for making it easier to handle tests. 7 | pub mod util; 8 | pub use util::{Retry, TestCommand, TestProject}; 9 | 10 | pub use ethers_solc; 11 | -------------------------------------------------------------------------------- /cli/tests/it/cast.rs: -------------------------------------------------------------------------------- 1 | //! Contains various tests for checking cast commands 2 | use std::env; 3 | 4 | use foundry_cli_test_utils::{ 5 | casttest, 6 | util::{TestCommand, TestProject}, 7 | }; 8 | 9 | // tests that the `cast find-block` command works correctly 10 | casttest!(finds_block, |_: TestProject, mut cmd: TestCommand| { 11 | // Skip fork tests if the RPC url is not set. 12 | if std::env::var("ETH_RPC_URL").is_err() { 13 | eprintln!("Skipping test finds_block. ETH_RPC_URL is not set."); 14 | return 15 | }; 16 | 17 | // Construct args 18 | let timestamp = "1647843609".to_string(); 19 | let eth_rpc_url = env::var("ETH_RPC_URL").unwrap(); 20 | 21 | // Call `cast find-block` 22 | cmd.args(["find-block", "--rpc-url", eth_rpc_url.as_str(), ×tamp]); 23 | let output = cmd.stdout_lossy(); 24 | println!("{output}"); 25 | 26 | // Expect successful block query 27 | // Query: 1647843609, Mar 21 2022 06:20:09 UTC 28 | // Output block: https://etherscan.io/block/14428082 29 | // Output block time: Mar 21 2022 06:20:09 UTC 30 | assert!(output.contains("14428082"), "{}", output); 31 | }); 32 | -------------------------------------------------------------------------------- /cli/tests/it/integration.rs: -------------------------------------------------------------------------------- 1 | use foundry_cli_test_utils::forgetest_external; 2 | 3 | forgetest_external!(solmate, "Rari-Capital/solmate"); 4 | forgetest_external!(geb, "reflexer-labs/geb"); 5 | forgetest_external!(stringutils, "Arachnid/solidity-stringutils"); 6 | // forgetest_external!(vaults, "Rari-Capital/vaults"); 7 | forgetest_external!(multicall, "makerdao/multicall", &["--block-number", "1"]); 8 | forgetest_external!(lootloose, "gakonst/lootloose"); 9 | forgetest_external!(lil_web3, "m1guelpf/lil-web3"); 10 | forgetest_external!(maple_loan, "maple-labs/loan"); 11 | 12 | // Forking tests 13 | forgetest_external!(drai, "mds1/drai", 13633752, &["--chain-id", "99"]); 14 | forgetest_external!(gunilev, "hexonaut/guni-lev", 13633752); 15 | -------------------------------------------------------------------------------- /cli/tests/it/main.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(feature = "external-integration-tests"))] 2 | mod cast; 3 | #[cfg(not(feature = "external-integration-tests"))] 4 | mod cmd; 5 | #[cfg(not(feature = "external-integration-tests"))] 6 | mod config; 7 | #[cfg(not(feature = "external-integration-tests"))] 8 | mod test; 9 | #[cfg(not(feature = "external-integration-tests"))] 10 | mod verify; 11 | 12 | #[cfg(feature = "external-integration-tests")] 13 | mod integration; 14 | 15 | fn main() {} 16 | -------------------------------------------------------------------------------- /cli/tests/it/test.rs: -------------------------------------------------------------------------------- 1 | //! Contains various tests for checking `forge test` 2 | use foundry_cli_test_utils::{ 3 | forgetest, 4 | util::{TestCommand, TestProject}, 5 | }; 6 | 7 | // import forge utils as mod 8 | #[allow(unused)] 9 | #[path = "../../src/utils.rs"] 10 | mod forge_utils; 11 | 12 | // tests that direct import paths are handled correctly 13 | forgetest!(can_fuzz_array_params, |prj: TestProject, mut cmd: TestCommand| { 14 | prj.insert_ds_test(); 15 | 16 | prj.inner() 17 | .add_source( 18 | "ATest.t.sol", 19 | r#" 20 | // SPDX-License-Identifier: UNLICENSED 21 | pragma solidity 0.8.10; 22 | import "./test.sol"; 23 | contract ATest is DSTest { 24 | function testArray(uint64[2] calldata values) external { 25 | assertTrue(true); 26 | } 27 | } 28 | "#, 29 | ) 30 | .unwrap(); 31 | 32 | cmd.arg("test"); 33 | cmd.stdout().contains("[PASS]") 34 | }); 35 | 36 | // tests that `bytecode_hash` will be sanitized 37 | forgetest!(can_test_pre_bytecode_hash, |prj: TestProject, mut cmd: TestCommand| { 38 | prj.insert_ds_test(); 39 | 40 | prj.inner() 41 | .add_source( 42 | "ATest.t.sol", 43 | r#" 44 | // SPDX-License-Identifier: UNLICENSED 45 | // pre bytecode hash version, was introduced in 0.6.0 46 | pragma solidity 0.5.17; 47 | import "./test.sol"; 48 | contract ATest is DSTest { 49 | function testArray(uint64[2] calldata values) external { 50 | assertTrue(true); 51 | } 52 | } 53 | "#, 54 | ) 55 | .unwrap(); 56 | 57 | cmd.arg("test"); 58 | cmd.stdout().contains("[PASS]") 59 | }); 60 | 61 | // tests that using the --match-path option only runs files matching the path 62 | forgetest!(can_test_with_match_path, |prj: TestProject, mut cmd: TestCommand| { 63 | prj.insert_ds_test(); 64 | 65 | prj.inner() 66 | .add_source( 67 | "ATest.t.sol", 68 | r#" 69 | // SPDX-License-Identifier: UNLICENSED 70 | pragma solidity 0.8.10; 71 | import "./test.sol"; 72 | contract ATest is DSTest { 73 | function testArray(uint64[2] calldata values) external { 74 | assertTrue(true); 75 | } 76 | } 77 | "#, 78 | ) 79 | .unwrap(); 80 | 81 | prj.inner() 82 | .add_source( 83 | "FailTest.t.sol", 84 | r#" 85 | // SPDX-License-Identifier: UNLICENSED 86 | pragma solidity 0.8.10; 87 | import "./test.sol"; 88 | contract FailTest is DSTest { 89 | function testNothing() external { 90 | assertTrue(false); 91 | } 92 | } 93 | "#, 94 | ) 95 | .unwrap(); 96 | 97 | cmd.args(["test", "--match-path", "*src/ATest.t.sol"]); 98 | cmd.stdout().contains("[PASS]") && !cmd.stdout().contains("[FAIL]") 99 | }); 100 | -------------------------------------------------------------------------------- /common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foundry-common" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | readme = "README.md" 7 | description = """ 8 | Common utilities for foundry's tools. 9 | """ 10 | repository = "https://github.com/gakonst/foundry" 11 | 12 | [dependencies] 13 | # foundry internal 14 | foundry-config = { path = "../config" } 15 | 16 | # eth 17 | ethers-core = { git = "https://github.com/gakonst/ethers-rs", default-features = false } 18 | 19 | # cli 20 | clap = { version = "3.0.10", features = [ 21 | "derive", 22 | "env", 23 | "unicode", 24 | "wrap_help", 25 | ] } 26 | 27 | # misc 28 | serde = "1.0.133" -------------------------------------------------------------------------------- /common/README.md: -------------------------------------------------------------------------------- 1 | Common utilities for building and using foundry's tools. -------------------------------------------------------------------------------- /common/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Common utilities for building and using foundry's tools. 2 | 3 | #![deny(missing_docs, unsafe_code, unused_crate_dependencies)] 4 | 5 | pub mod evm; 6 | -------------------------------------------------------------------------------- /config/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foundry-config" 3 | version = "0.2.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | description = """ 7 | Foundry configuration 8 | """ 9 | repository = "https://github.com/gakonst/foundry" 10 | readme = "README.md" 11 | 12 | [dependencies] 13 | dirs-next = "2.0.0" 14 | semver = { version = "1.0.5", features = ["serde"] } 15 | serde = { version = "1.0", features = ["derive"] } 16 | toml = "0.5" 17 | figment = { version = "0.10", features = ["toml", "env"] } 18 | eyre = "0.6.5" 19 | ethers-core = { git = "https://github.com/gakonst/ethers-rs", default-features = false } 20 | ethers-solc = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["async", "svm-solc"] } 21 | Inflector = "0.11.4" 22 | regex = "1.5.5" 23 | 24 | [dev-dependencies] 25 | pretty_assertions = "1.0.0" 26 | figment = { version = "0.10", features = ["test"] } 27 | -------------------------------------------------------------------------------- /config/README.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Foundry's configuration system allows you to configure it's tools the way _you_ want while also providing with a 4 | sensible set of defaults. 5 | 6 | ## Profiles 7 | 8 | Configurations can be arbitrarily namespaced by profiles. Foundry's default config is also named `default`, but can 9 | arbitrarily name and configure profiles as you like and set the `FOUNDRY_PROFILE` environment variable to the selected 10 | profile's name. This results in foundry's tools (forge) preferring the values in the profile with the named that's set 11 | in `FOUNDRY_PROFILE`. 12 | 13 | ## foundry.toml 14 | 15 | Foundry's tools search for a `foundry.toml` or the filename in a `FOUNDRY_CONFIG` environment variable starting at the 16 | current working directory. If it is not found, the parent directory, its parent directory, and so on are searched until 17 | the file is found or the root is reached. But the typical location for the global `foundry.toml` would 18 | be `~/.foundry/foundry.toml`, which is also checked. If the path set in `FOUNDRY_CONFIG` is absolute, no such search 19 | takes place and the absolute path is used directly. 20 | 21 | In `foundry.toml` you can define multiple profiles, therefore the file is assumed to be _nested_, so each top-level key 22 | declares a profile and its values configure the profile. 23 | 24 | The following is an example of what such a file might look like. This can also be obtained with `forge config` 25 | 26 | ```toml 27 | ## defaults for _all_ profiles 28 | [default] 29 | src = "src" 30 | out = "out" 31 | libs = ["lib"] 32 | solc = "0.8.10" # to use a specific local solc install set the path as `solc = "/solc"` 33 | eth-rpc-url = "https://mainnet.infura.io" 34 | 35 | ## set only when the `hardhat` profile is selected 36 | [hardhat] 37 | src = "contracts" 38 | out = "artifacts" 39 | libs = ["node_modules"] 40 | 41 | ## set only when the `spells` profile is selected 42 | [spells] 43 | ## --snip-- more settings 44 | ``` 45 | 46 | ## Default profile 47 | 48 | When determining the profile to use, `Config` considers the following sources in ascending priority order to read from 49 | and merge, at the per-key level: 50 | 51 | 1. [`Config::default()`], which provides default values for all parameters. 52 | 2. `foundry.toml` _or_ TOML file path in `FOUNDRY_CONFIG` environment variable. 53 | 3. `FOUNDRY_` or `DAPP_` prefixed environment variables. 54 | 55 | The selected profile is the value of the `FOUNDRY_PROFILE` environment variable, or if it is not set, "default". 56 | 57 | #### All Options 58 | 59 | The following is a foundry.toml file with all configuration options set. 60 | 61 | ```toml 62 | ## defaults for _all_ profiles 63 | [default] 64 | src = 'src' 65 | test = 'test' 66 | out = 'out' 67 | libs = ['lib'] 68 | remappings = [] 69 | libraries = [] 70 | cache = true 71 | cache_path = 'cache' 72 | force = false 73 | evm_version = 'london' 74 | gas_reports = ['*'] 75 | ## Sets the concrete solc version to use, this overrides the `auto_detect_solc` value 76 | # solc_version = '0.8.10' 77 | auto_detect_solc = true 78 | offline = false 79 | optimizer = true 80 | optimizer_runs = 200 81 | via_ir = false 82 | verbosity = 0 83 | ignored_error_codes = [] 84 | fuzz_runs = 256 85 | ffi = false 86 | sender = '0x00a329c0648769a73afac7f9381e08fb43dbea72' 87 | tx_origin = '0x00a329c0648769a73afac7f9381e08fb43dbea72' 88 | initial_balance = '0xffffffffffffffffffffffff' 89 | block_number = 0 90 | # NOTE due to a toml-rs limitation, this value needs to be a string if the desired gas limit exceeds `i64::MAX` (9223372036854775807) 91 | gas_limit = 9223372036854775807 92 | gas_price = 0 93 | block_base_fee_per_gas = 0 94 | block_coinbase = '0x0000000000000000000000000000000000000000' 95 | block_timestamp = 0 96 | block_difficulty = 0 97 | # caches storage retrieved locally for certain chains and endpoints 98 | # can also be restrictied to `chains = ["optimism", "mainnet"]` 99 | # by default all endpoints will be cached, alternative options are "remote" for only caching non localhost endpoints and "" 100 | # to disable storage caching entirely set `no_storage_caching = true` 101 | rpc_storage_caching = { chains = "all", endpoints = "all" } 102 | # this overrides `rpc_storage_caching` entirely 103 | no_storage_caching = false 104 | # use ipfs method to generate the metadata hash, solc's default. 105 | # To not include the metadata hash, to allow for deterministic code: https://docs.soliditylang.org/en/latest/metadata.html, use "none" 106 | bytecode_hash = "ipfs" 107 | # If this option is enabled, Solc is instructed to generate output (bytecode) only for the required contracts 108 | # this can reduce compile time for `forge test` a bit but is considered experimental at this point. 109 | sparse_mode = false 110 | ``` 111 | 112 | ##### Additional Optimizer settings 113 | 114 | Optimizer components can be tweaked with the `OptimizerDetails` object: 115 | 116 | See [Compiler Input Description `settings.optimizer.details`](https://docs.soliditylang.org/en/latest/using-the-compiler.html#compiler-input-and-output-json-description) 117 | 118 | The `optimizer_details` (`optimizerDetails` also works) settings must be prefixed with the profile they correspond 119 | to: `[default.optimizer_details]` 120 | belongs to the `[default]` profile 121 | 122 | ```toml 123 | [default.optimizer_details] 124 | constantOptimizer = true 125 | yul = true 126 | # this sets the `yulDetails` of the `optimizer_details` for the `default` profile 127 | [default.optimizer_details.yulDetails] 128 | stackAllocation = true 129 | optimizerSteps = 'dhfoDgvulfnTUtnIf' 130 | ``` 131 | 132 | ## Environment Variables 133 | 134 | Foundry's tools read all environment variable names prefixed with `FOUNDRY_` using the string after the `_` as the name 135 | of a configuration value as the value of the parameter as the value itself. But the 136 | corresponding [dapptools](https://github.com/dapphub/dapptools/tree/master/src/dapp#configuration) config vars are also 137 | supported, this means that `FOUNDRY_SRC` and `DAPP_SRC` are equivalent. 138 | 139 | Environment variables take precedence over values in `foundry.toml`. Values are parsed as loose form of TOML syntax. 140 | Consider the following examples: 141 | -------------------------------------------------------------------------------- /config/src/chain.rs: -------------------------------------------------------------------------------- 1 | use ethers_core::types::ParseChainError; 2 | use serde::{Deserialize, Deserializer, Serialize}; 3 | use std::{fmt, str::FromStr}; 4 | 5 | /// Either a named or chain id or the actual id value 6 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] 7 | #[serde(untagged)] 8 | pub enum Chain { 9 | /// Contains a known chain 10 | #[serde(serialize_with = "super::from_str_lowercase::serialize")] 11 | Named(ethers_core::types::Chain), 12 | /// Contains the id of a chain 13 | Id(u64), 14 | } 15 | 16 | impl Chain { 17 | /// The id of the chain 18 | pub fn id(&self) -> u64 { 19 | match self { 20 | Chain::Named(chain) => *chain as u64, 21 | Chain::Id(id) => *id, 22 | } 23 | } 24 | } 25 | 26 | impl fmt::Display for Chain { 27 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 28 | match self { 29 | Chain::Named(chain) => chain.fmt(f), 30 | Chain::Id(id) => { 31 | if let Ok(chain) = ethers_core::types::Chain::try_from(*id) { 32 | chain.fmt(f) 33 | } else { 34 | id.fmt(f) 35 | } 36 | } 37 | } 38 | } 39 | } 40 | 41 | impl From for Chain { 42 | fn from(id: ethers_core::types::Chain) -> Self { 43 | Chain::Named(id) 44 | } 45 | } 46 | 47 | impl From for Chain { 48 | fn from(id: u64) -> Self { 49 | Chain::Id(id) 50 | } 51 | } 52 | 53 | impl From for u64 { 54 | fn from(c: Chain) -> Self { 55 | match c { 56 | Chain::Named(c) => c as u64, 57 | Chain::Id(id) => id, 58 | } 59 | } 60 | } 61 | 62 | impl TryFrom for ethers_core::types::Chain { 63 | type Error = ParseChainError; 64 | 65 | fn try_from(chain: Chain) -> Result { 66 | match chain { 67 | Chain::Named(chain) => Ok(chain), 68 | Chain::Id(id) => id.try_into(), 69 | } 70 | } 71 | } 72 | 73 | impl FromStr for Chain { 74 | type Err = String; 75 | 76 | fn from_str(s: &str) -> Result { 77 | if let Ok(chain) = ethers_core::types::Chain::from_str(s) { 78 | Ok(Chain::Named(chain)) 79 | } else { 80 | s.parse::() 81 | .map(Chain::Id) 82 | .map_err(|_| format!("Expected known chain or integer, found: {s}")) 83 | } 84 | } 85 | } 86 | 87 | impl<'de> Deserialize<'de> for Chain { 88 | fn deserialize(deserializer: D) -> Result 89 | where 90 | D: Deserializer<'de>, 91 | { 92 | #[derive(Deserialize)] 93 | #[serde(untagged)] 94 | enum ChainId { 95 | Named(String), 96 | Id(u64), 97 | } 98 | 99 | match ChainId::deserialize(deserializer)? { 100 | ChainId::Named(s) => { 101 | s.to_lowercase().parse().map(Chain::Named).map_err(serde::de::Error::custom) 102 | } 103 | ChainId::Id(id) => Ok(ethers_core::types::Chain::try_from(id) 104 | .map(Chain::Named) 105 | .unwrap_or_else(|_| Chain::Id(id))), 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /config/src/macros.rs: -------------------------------------------------------------------------------- 1 | /// A macro to implement converters from a type to [`Config`] and [`figment::Figment`] 2 | /// 3 | /// This can be used to remove some boilerplate code that's necessary to add additional layer(s) to 4 | /// the [`Config`]'s default `Figment`. 5 | /// 6 | /// `impl_figment` takes the default `Config` and merges additional `Provider`, therefore the 7 | /// targeted type, requires an implementation of `figment::Profile`. 8 | /// 9 | /// # Example 10 | /// 11 | /// Use `impl_figment` on a type with a `root: Option` field, which will be used for 12 | /// [`Config::figment_with_root()`] 13 | /// 14 | /// ```rust 15 | /// use std::path::PathBuf; 16 | /// use serde::Serialize; 17 | /// use foundry_config::{Config, impl_figment_convert}; 18 | /// use foundry_config::figment::*; 19 | /// use foundry_config::figment::error::Kind::InvalidType; 20 | /// use foundry_config::figment::value::*; 21 | /// #[derive(Default, Serialize)] 22 | /// struct MyArgs { 23 | /// #[serde(skip_serializing_if = "Option::is_none")] 24 | /// root: Option, 25 | /// } 26 | /// impl_figment_convert!(MyArgs); 27 | /// 28 | /// impl Provider for MyArgs { 29 | /// fn metadata(&self) -> Metadata { 30 | /// Metadata::default() 31 | /// } 32 | /// 33 | /// fn data(&self) -> Result, Error> { 34 | /// let value = Value::serialize(self)?; 35 | /// let error = InvalidType(value.to_actual(), "map".into()); 36 | /// let mut dict = value.into_dict().ok_or(error)?; 37 | /// Ok(Map::from([(Config::selected_profile(), dict)])) 38 | /// } 39 | /// } 40 | /// 41 | /// let figment: Figment = From::from(&MyArgs::default()); 42 | /// let config: Config = From::from(&MyArgs::default()); 43 | /// 44 | /// // Use `impl_figment` on a type that has several nested `Provider` as fields. 45 | /// 46 | /// #[derive(Default)] 47 | /// struct Outer { 48 | /// start: MyArgs, 49 | /// second: MyArgs, 50 | /// third: MyArgs, 51 | /// } 52 | /// impl_figment_convert!(Outer, start, second, third); 53 | /// 54 | /// let figment: Figment = From::from(&Outer::default()); 55 | /// let config: Config = From::from(&Outer::default()); 56 | /// ``` 57 | #[macro_export] 58 | macro_rules! impl_figment_convert { 59 | ($name:ty) => { 60 | impl<'a> From<&'a $name> for $crate::figment::Figment { 61 | fn from(args: &'a $name) -> Self { 62 | if let Some(root) = args.root.clone() { 63 | $crate::Config::figment_with_root(root) 64 | } else { 65 | $crate::Config::figment_with_root($crate::find_project_root_path().unwrap()) 66 | } 67 | .merge(args) 68 | } 69 | } 70 | 71 | impl<'a> From<&'a $name> for $crate::Config { 72 | fn from(args: &'a $name) -> Self { 73 | let figment: $crate::figment::Figment = args.into(); 74 | $crate::Config::from_provider(figment).sanitized() 75 | } 76 | } 77 | }; 78 | ($name:ty, $start:ident $(, $more:ident)*) => { 79 | impl<'a> From<&'a $name> for $crate::figment::Figment { 80 | fn from(args: &'a $name) -> Self { 81 | let mut figment: $crate::figment::Figment = From::from(&args.$start); 82 | $ ( 83 | figment = figment.merge(&args.$more); 84 | )* 85 | figment 86 | } 87 | } 88 | 89 | impl<'a> From<&'a $name> for $crate::Config { 90 | fn from(args: &'a $name) -> Self { 91 | let figment: $crate::figment::Figment = args.into(); 92 | $crate::Config::from_provider(figment).sanitized() 93 | } 94 | } 95 | }; 96 | } 97 | /// A macro to implement converters from a type to [`Config`] and [`figment::Figment`] 98 | #[macro_export] 99 | macro_rules! impl_figment_convert_cast { 100 | ($name:ty) => { 101 | impl<'a> From<&'a $name> for $crate::figment::Figment { 102 | fn from(args: &'a $name) -> Self { 103 | $crate::Config::figment_with_root($crate::find_project_root_path().unwrap()) 104 | .merge(args) 105 | } 106 | } 107 | 108 | impl<'a> From<&'a $name> for $crate::Config { 109 | fn from(args: &'a $name) -> Self { 110 | let figment: $crate::figment::Figment = args.into(); 111 | $crate::Config::from_provider(figment).sanitized() 112 | } 113 | } 114 | }; 115 | } 116 | -------------------------------------------------------------------------------- /config/src/utils.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions 2 | 3 | use std::{collections::BTreeMap, path::PathBuf, str::FromStr}; 4 | 5 | use crate::Config; 6 | use ethers_solc::{ 7 | error::SolcError, 8 | remappings::{Remapping, RemappingError}, 9 | }; 10 | use figment::value::Value; 11 | 12 | /// Loads the config for the current project workspace 13 | pub fn load_config() -> Config { 14 | load_config_with_root(None) 15 | } 16 | 17 | /// Loads the config for the current project workspace or the provided root path 18 | pub fn load_config_with_root(root: Option) -> Config { 19 | if let Some(root) = root { 20 | Config::load_with_root(root) 21 | } else { 22 | Config::load_with_root(find_project_root_path().unwrap()) 23 | } 24 | .sanitized() 25 | } 26 | 27 | /// Returns the path of the top-level directory of the working git tree. If there is no working 28 | /// tree, an error is returned. 29 | pub fn find_git_root_path() -> eyre::Result { 30 | let path = 31 | std::process::Command::new("git").args(&["rev-parse", "--show-toplevel"]).output()?.stdout; 32 | let path = std::str::from_utf8(&path)?.trim_end_matches('\n'); 33 | Ok(PathBuf::from(path)) 34 | } 35 | 36 | /// Returns the root path to set for the project root 37 | /// 38 | /// traverse the dir tree up and look for a `foundry.toml` file starting at the cwd, but only until 39 | /// the root dir of the current repo so that 40 | /// 41 | /// ```text 42 | /// -- foundry.toml 43 | /// 44 | /// -- repo 45 | /// |__ .git 46 | /// |__sub 47 | /// |__ cwd 48 | /// ``` 49 | /// will still detect `repo` as root 50 | pub fn find_project_root_path() -> std::io::Result { 51 | let boundary = find_git_root_path().unwrap_or_else(|_| std::env::current_dir().unwrap()); 52 | let cwd = std::env::current_dir()?; 53 | let mut cwd = cwd.as_path(); 54 | // traverse as long as we're in the current git repo cwd 55 | while cwd.starts_with(&boundary) { 56 | let file_path = cwd.join(Config::FILE_NAME); 57 | if file_path.is_file() { 58 | return Ok(cwd.to_path_buf()) 59 | } 60 | if let Some(parent) = cwd.parent() { 61 | cwd = parent; 62 | } else { 63 | break 64 | } 65 | } 66 | // no foundry.toml found 67 | Ok(boundary) 68 | } 69 | 70 | /// Returns all [`Remapping`]s contained in the `remappings` str separated by newlines 71 | /// 72 | /// # Example 73 | /// 74 | /// ``` 75 | /// use foundry_config::remappings_from_newline; 76 | /// let remappings: Result, _> = remappings_from_newline( 77 | /// r#" 78 | /// file-ds-test/=lib/ds-test/ 79 | /// file-other/=lib/other/ 80 | /// "#, 81 | /// ) 82 | /// .collect(); 83 | /// ``` 84 | pub fn remappings_from_newline( 85 | remappings: &str, 86 | ) -> impl Iterator> + '_ { 87 | remappings.lines().map(|x| x.trim()).filter(|x| !x.is_empty()).map(Remapping::from_str) 88 | } 89 | 90 | /// Returns the remappings from the given var 91 | /// 92 | /// Returns `None` if the env var is not set, otherwise all Remappings, See 93 | /// `remappings_from_newline` 94 | pub fn remappings_from_env_var(env_var: &str) -> Option, RemappingError>> { 95 | let val = std::env::var(env_var).ok()?; 96 | Some(remappings_from_newline(&val).collect()) 97 | } 98 | 99 | /// Parses all libraries in the form of 100 | /// `::` 101 | /// 102 | /// # Example 103 | /// 104 | /// ``` 105 | /// use foundry_config::parse_libraries; 106 | /// let libs = parse_libraries(&[ 107 | /// "src/DssSpell.sol:DssExecLib:0xfD88CeE74f7D78697775aBDAE53f9Da1559728E4".to_string(), 108 | /// ]) 109 | /// .unwrap(); 110 | /// ``` 111 | pub fn parse_libraries( 112 | libs: &[String], 113 | ) -> Result>, SolcError> { 114 | let mut libraries = BTreeMap::default(); 115 | for lib in libs { 116 | let mut items = lib.split(':'); 117 | let file = items 118 | .next() 119 | .ok_or_else(|| SolcError::msg(format!("failed to parse invalid library: {lib}")))?; 120 | let lib = items 121 | .next() 122 | .ok_or_else(|| SolcError::msg(format!("failed to parse invalid library: {lib}")))?; 123 | let addr = items 124 | .next() 125 | .ok_or_else(|| SolcError::msg(format!("failed to parse invalid library: {lib}")))?; 126 | if items.next().is_some() { 127 | return Err(SolcError::msg(format!("failed to parse invalid library: {lib}"))) 128 | } 129 | libraries 130 | .entry(file.to_string()) 131 | .or_insert_with(BTreeMap::default) 132 | .insert(lib.to_string(), addr.to_string()); 133 | } 134 | Ok(libraries) 135 | } 136 | 137 | /// Converts the `val` into a `figment::Value::Array` 138 | /// 139 | /// The values should be separated by commas, surrounding brackets are also supported `[a,b,c]` 140 | pub fn to_array_value(val: &str) -> Result { 141 | let value: Value = match Value::from(val) { 142 | Value::String(_, val) => val 143 | .trim_start_matches('[') 144 | .trim_end_matches(']') 145 | .split(',') 146 | .map(|s| s.to_string()) 147 | .collect::>() 148 | .into(), 149 | Value::Empty(_, _) => Vec::::new().into(), 150 | val @ Value::Array(_, _) => val, 151 | _ => return Err(format!("Invalid value `{val}`, expected an array").into()), 152 | }; 153 | Ok(value) 154 | } 155 | -------------------------------------------------------------------------------- /evm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foundry-evm" 3 | version = "0.2.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | readme = "../README.md" 7 | repository = "https://github.com/gakonst/foundry" 8 | keywords = ["ethereum", "web3", "evm"] 9 | 10 | # TODO: We can probably reduce dependencies here or in the forge crate 11 | [dependencies] 12 | foundry-utils = { path = "./../utils" } 13 | 14 | # Encoding/decoding 15 | serde_json = "1.0.67" 16 | serde = "1.0.130" 17 | hex = "0.4.3" 18 | ethers = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["solc-full"] } 19 | 20 | # Error handling 21 | eyre = "0.6.5" 22 | thiserror = "1.0.29" 23 | 24 | # Logging 25 | tracing = "0.1.26" 26 | tracing-subscriber = "=0.3.9" 27 | tracing-error = "0.2.0" 28 | 29 | # Threading/futures 30 | tokio = { version = "1.10.1" } 31 | parking_lot = "0.12.0" 32 | futures = "0.3.21" 33 | once_cell = "1.9.0" 34 | 35 | # EVM 36 | bytes = "1.1.0" 37 | hashbrown = "0.12" 38 | revm = { package = "revm", git = "https://github.com/bluealloy/revm", default-features = false, features = ["std", "k256", "with-serde", "memory_limit"] } 39 | 40 | # Fuzzer 41 | proptest = "1.0.0" 42 | 43 | # Display 44 | ansi_term = "0.12.1" 45 | url = "2.2.2" 46 | 47 | [dev-dependencies] 48 | tempfile = "3.3.0" 49 | -------------------------------------------------------------------------------- /evm/src/decode.rs: -------------------------------------------------------------------------------- 1 | //! Various utilities to decode test results 2 | use crate::abi::ConsoleEvents::{self, *}; 3 | use ethers::{abi::RawLog, contract::EthLogDecode}; 4 | 5 | /// Decode a set of logs, only returning logs from DSTest logging events and Hardhat's `console.log` 6 | pub fn decode_console_logs(logs: &[RawLog]) -> Vec { 7 | logs.iter().filter_map(decode_console_log).collect() 8 | } 9 | 10 | /// Decode a single log. 11 | /// 12 | /// This function returns [None] if it is not a DSTest log or the result of a Hardhat 13 | /// `console.log`. 14 | pub fn decode_console_log(log: &RawLog) -> Option { 15 | let decoded = match ConsoleEvents::decode_log(log).ok()? { 16 | LogsFilter(inner) => format!("{}", inner.0), 17 | LogBytesFilter(inner) => format!("{}", inner.0), 18 | LogNamedAddressFilter(inner) => format!("{}: {:?}", inner.key, inner.val), 19 | LogNamedBytes32Filter(inner) => { 20 | format!("{}: 0x{}", inner.key, hex::encode(inner.val)) 21 | } 22 | LogNamedDecimalIntFilter(inner) => { 23 | let (sign, val) = inner.val.into_sign_and_abs(); 24 | format!( 25 | "{}: {}{}", 26 | inner.key, 27 | sign, 28 | ethers::utils::format_units(val, inner.decimals.as_u32()).unwrap() 29 | ) 30 | } 31 | LogNamedDecimalUintFilter(inner) => { 32 | format!( 33 | "{}: {}", 34 | inner.key, 35 | ethers::utils::format_units(inner.val, inner.decimals.as_u32()).unwrap() 36 | ) 37 | } 38 | LogNamedIntFilter(inner) => format!("{}: {:?}", inner.key, inner.val), 39 | LogNamedUintFilter(inner) => format!("{}: {:?}", inner.key, inner.val), 40 | LogNamedBytesFilter(inner) => { 41 | format!("{}: 0x{}", inner.key, hex::encode(inner.val)) 42 | } 43 | LogNamedStringFilter(inner) => format!("{}: {}", inner.key, inner.val), 44 | 45 | e => e.to_string(), 46 | }; 47 | Some(decoded) 48 | } 49 | -------------------------------------------------------------------------------- /evm/src/executor/builder.rs: -------------------------------------------------------------------------------- 1 | use ethers::prelude::Provider; 2 | use revm::{ 3 | db::{DatabaseRef, EmptyDB}, 4 | Env, SpecId, 5 | }; 6 | use std::{path::PathBuf, sync::Arc}; 7 | 8 | use super::{ 9 | fork::SharedBackend, 10 | inspector::{Cheatcodes, InspectorStackConfig}, 11 | Executor, 12 | }; 13 | 14 | use ethers::types::{H160, H256, U256}; 15 | 16 | use crate::executor::fork::{BlockchainDb, BlockchainDbMeta}; 17 | 18 | use revm::AccountInfo; 19 | 20 | #[derive(Default, Debug)] 21 | pub struct ExecutorBuilder { 22 | /// The execution environment configuration. 23 | env: Env, 24 | /// The configuration used to build an [InspectorStack]. 25 | inspector_config: InspectorStackConfig, 26 | gas_limit: Option, 27 | } 28 | 29 | /// Represents a _fork_ of a live chain whose data is available only via the `url` endpoint. 30 | /// 31 | /// *Note:* this type intentionally does not implement `Clone` to prevent [Fork::spawn_backend()] 32 | /// from being called multiple times. 33 | #[derive(Debug)] 34 | pub struct Fork { 35 | /// Where to read the cached storage from 36 | pub cache_path: Option, 37 | /// The URL to a node for fetching remote state 38 | pub url: String, 39 | /// The block to fork against 40 | pub pin_block: Option, 41 | /// chain id retrieved from the endpoint 42 | pub chain_id: u64, 43 | } 44 | 45 | impl Fork { 46 | /// Initialises and spawns the Storage Backend, the [revm::Database] 47 | /// 48 | /// If configured, then this will initialise the backend with the storage cache. 49 | /// 50 | /// The `SharedBackend` returned is connected to a background thread that communicates with the 51 | /// endpoint via channels and is intended to be cloned when multiple [revm::Database] are 52 | /// required. See also [crate::executor::fork::SharedBackend] 53 | pub async fn spawn_backend(self, env: &Env) -> SharedBackend { 54 | let Fork { cache_path, url, pin_block, chain_id } = self; 55 | 56 | let provider = 57 | Arc::new(Provider::try_from(url.clone()).expect("Failed to establish provider")); 58 | 59 | let mut meta = BlockchainDbMeta::new(env.clone(), url); 60 | 61 | // update the meta to match the forked config 62 | meta.cfg_env.chain_id = chain_id.into(); 63 | if let Some(pin) = pin_block { 64 | meta.block_env.number = pin.into(); 65 | } 66 | 67 | let db = BlockchainDb::new(meta, cache_path); 68 | 69 | SharedBackend::spawn_backend(provider, db, pin_block.map(Into::into)).await 70 | } 71 | } 72 | /// Variants of a [revm::Database] 73 | #[derive(Debug, Clone)] 74 | pub enum Backend { 75 | /// Simple in memory [revm::Database] 76 | Simple(EmptyDB), 77 | /// A [revm::Database] that forks of a remote location and can have multiple consumers of the 78 | /// same data 79 | Forked(SharedBackend), 80 | } 81 | 82 | impl Backend { 83 | /// Instantiates a new backend union based on whether there was or not a fork url specified 84 | pub async fn new(fork: Option, env: &Env) -> Self { 85 | if let Some(fork) = fork { 86 | Backend::Forked(fork.spawn_backend(env).await) 87 | } else { 88 | Self::simple() 89 | } 90 | } 91 | 92 | pub fn simple() -> Self { 93 | Backend::Simple(EmptyDB()) 94 | } 95 | } 96 | 97 | impl DatabaseRef for Backend { 98 | fn basic(&self, address: H160) -> AccountInfo { 99 | match self { 100 | Backend::Simple(inner) => inner.basic(address), 101 | Backend::Forked(inner) => inner.basic(address), 102 | } 103 | } 104 | 105 | fn code_by_hash(&self, address: H256) -> bytes::Bytes { 106 | match self { 107 | Backend::Simple(inner) => inner.code_by_hash(address), 108 | Backend::Forked(inner) => inner.code_by_hash(address), 109 | } 110 | } 111 | 112 | fn storage(&self, address: H160, index: U256) -> U256 { 113 | match self { 114 | Backend::Simple(inner) => inner.storage(address, index), 115 | Backend::Forked(inner) => inner.storage(address, index), 116 | } 117 | } 118 | 119 | fn block_hash(&self, number: U256) -> H256 { 120 | match self { 121 | Backend::Simple(inner) => inner.block_hash(number), 122 | Backend::Forked(inner) => inner.block_hash(number), 123 | } 124 | } 125 | } 126 | 127 | impl ExecutorBuilder { 128 | #[must_use] 129 | pub fn new() -> Self { 130 | Default::default() 131 | } 132 | 133 | /// Enables cheatcodes on the executor. 134 | #[must_use] 135 | pub fn with_cheatcodes(mut self, ffi: bool) -> Self { 136 | self.inspector_config.cheatcodes = Some(Cheatcodes::new(ffi, self.env.block.clone())); 137 | self 138 | } 139 | 140 | /// Enables tracing 141 | #[must_use] 142 | pub fn with_tracing(mut self) -> Self { 143 | self.inspector_config.tracing = true; 144 | self 145 | } 146 | 147 | /// Enables the debugger 148 | #[must_use] 149 | pub fn with_debugger(mut self) -> Self { 150 | self.inspector_config.debugger = true; 151 | self 152 | } 153 | 154 | /// Sets the EVM spec to use 155 | #[must_use] 156 | pub fn with_spec(mut self, spec: SpecId) -> Self { 157 | self.env.cfg.spec_id = spec; 158 | self 159 | } 160 | 161 | /// Sets the executor gas limit. 162 | /// 163 | /// See [Executor::gas_limit] for more info on why you might want to set this. 164 | #[must_use] 165 | pub fn with_gas_limit(mut self, gas_limit: U256) -> Self { 166 | self.gas_limit = Some(gas_limit); 167 | self 168 | } 169 | 170 | /// Configure the execution environment (gas limit, chain spec, ...) 171 | #[must_use] 172 | pub fn with_config(mut self, env: Env) -> Self { 173 | self.inspector_config.block = env.block.clone(); 174 | self.env = env; 175 | self 176 | } 177 | 178 | /// Builds the executor as configured. 179 | pub fn build(self, db: impl Into) -> Executor { 180 | let gas_limit = self.gas_limit.unwrap_or(self.env.block.gas_limit); 181 | Executor::new(db.into(), self.env, self.inspector_config, gas_limit) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /evm/src/executor/fork/init.rs: -------------------------------------------------------------------------------- 1 | use ethers::{providers::Middleware, types::Address}; 2 | use revm::{BlockEnv, CfgEnv, Env, TxEnv}; 3 | 4 | /// Initializes a REVM block environment based on a forked 5 | /// ethereum provider. 6 | pub async fn environment( 7 | provider: &M, 8 | memory_limit: u64, 9 | override_chain_id: Option, 10 | pin_block: Option, 11 | origin: Address, 12 | ) -> Result { 13 | let block_number = if let Some(pin_block) = pin_block { 14 | pin_block 15 | } else { 16 | provider.get_block_number().await?.as_u64() 17 | }; 18 | let (gas_price, rpc_chain_id, block) = tokio::try_join!( 19 | provider.get_gas_price(), 20 | provider.get_chainid(), 21 | provider.get_block(block_number) 22 | )?; 23 | let block = block.expect("block not found"); 24 | 25 | Ok(Env { 26 | cfg: CfgEnv { 27 | chain_id: override_chain_id.unwrap_or(rpc_chain_id.as_u64()).into(), 28 | memory_limit, 29 | ..Default::default() 30 | }, 31 | block: BlockEnv { 32 | number: block.number.expect("block number not found").as_u64().into(), 33 | timestamp: block.timestamp, 34 | coinbase: block.author, 35 | difficulty: block.difficulty, 36 | basefee: block.base_fee_per_gas.unwrap_or_default(), 37 | gas_limit: block.gas_limit, 38 | }, 39 | tx: TxEnv { 40 | caller: origin, 41 | gas_price, 42 | chain_id: Some(override_chain_id.unwrap_or(rpc_chain_id.as_u64())), 43 | gas_limit: block.gas_limit.as_u64(), 44 | ..Default::default() 45 | }, 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /evm/src/executor/fork/mod.rs: -------------------------------------------------------------------------------- 1 | mod backend; 2 | pub use backend::SharedBackend; 3 | 4 | mod init; 5 | pub use init::environment; 6 | 7 | mod cache; 8 | pub use cache::{BlockchainDb, BlockchainDbMeta, JsonBlockCacheDB}; 9 | -------------------------------------------------------------------------------- /evm/src/executor/inspector/cheatcodes/ext.rs: -------------------------------------------------------------------------------- 1 | use crate::abi::HEVMCalls; 2 | use bytes::Bytes; 3 | use ethers::{ 4 | abi::{self, AbiEncode, Token}, 5 | prelude::{artifacts::CompactContractBytecode, ProjectPathsConfig}, 6 | }; 7 | use serde::Deserialize; 8 | use std::{fs::File, io::Read, path::Path, process::Command}; 9 | 10 | fn ffi(args: &[String]) -> Result { 11 | let output = Command::new(&args[0]) 12 | .args(&args[1..]) 13 | .output() 14 | .map_err(|err| err.to_string().encode())? 15 | .stdout; 16 | let output = unsafe { std::str::from_utf8_unchecked(&output) }; 17 | let decoded = hex::decode(&output.trim().strip_prefix("0x").unwrap_or(output)) 18 | .map_err(|err| err.to_string().encode())?; 19 | 20 | Ok(abi::encode(&[Token::Bytes(decoded.to_vec())]).into()) 21 | } 22 | 23 | /// An enum which unifies the deserialization of Hardhat-style artifacts with Forge-style artifacts 24 | /// to get their bytecode. 25 | #[derive(Deserialize)] 26 | #[serde(untagged)] 27 | #[allow(clippy::large_enum_variant)] 28 | enum ArtifactBytecode { 29 | Hardhat(HardhatArtifact), 30 | Forge(CompactContractBytecode), 31 | } 32 | 33 | impl ArtifactBytecode { 34 | fn into_inner(self) -> Option { 35 | match self { 36 | ArtifactBytecode::Hardhat(inner) => Some(inner.bytecode), 37 | ArtifactBytecode::Forge(inner) => { 38 | inner.bytecode.and_then(|bytecode| bytecode.object.into_bytes()) 39 | } 40 | } 41 | } 42 | } 43 | 44 | /// A thin wrapper around a Hardhat-style artifact that only extracts the bytecode. 45 | #[derive(Deserialize)] 46 | struct HardhatArtifact { 47 | #[serde(deserialize_with = "ethers::solc::artifacts::deserialize_bytes")] 48 | bytecode: ethers::types::Bytes, 49 | } 50 | 51 | fn get_code(path: &str) -> Result { 52 | let path = if path.ends_with(".json") { 53 | Path::new(&path).to_path_buf() 54 | } else { 55 | let parts: Vec<&str> = path.split(':').collect(); 56 | let file = parts[0]; 57 | let contract_name = 58 | if parts.len() == 1 { parts[0].replace(".sol", "") } else { parts[1].to_string() }; 59 | let out_dir = ProjectPathsConfig::find_artifacts_dir(Path::new("./")); 60 | out_dir.join(format!("{file}/{contract_name}.json")) 61 | }; 62 | 63 | let mut buffer = String::new(); 64 | File::open(path) 65 | .map_err(|err| err.to_string().encode())? 66 | .read_to_string(&mut buffer) 67 | .map_err(|err| err.to_string().encode())?; 68 | 69 | let bytecode = serde_json::from_str::(&buffer) 70 | .map_err(|err| err.to_string().encode())?; 71 | 72 | if let Some(bin) = bytecode.into_inner() { 73 | Ok(abi::encode(&[Token::Bytes(bin.to_vec())]).into()) 74 | } else { 75 | Err("No bytecode for contract. Is it abstract or unlinked?".to_string().encode().into()) 76 | } 77 | } 78 | 79 | pub fn apply(ffi_enabled: bool, call: &HEVMCalls) -> Option> { 80 | Some(match call { 81 | HEVMCalls::Ffi(inner) => { 82 | if !ffi_enabled { 83 | Err("FFI disabled: run again with `--ffi` if you want to allow tests to call external scripts.".to_string().encode().into()) 84 | } else { 85 | ffi(&inner.0) 86 | } 87 | } 88 | HEVMCalls::GetCode(inner) => get_code(&inner.0), 89 | _ => return None, 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /evm/src/executor/inspector/cheatcodes/fuzz.rs: -------------------------------------------------------------------------------- 1 | use crate::{abi::HEVMCalls, fuzz::ASSUME_MAGIC_RETURN_CODE}; 2 | use bytes::Bytes; 3 | use revm::{Database, EVMData}; 4 | 5 | pub fn apply( 6 | _: &mut EVMData<'_, DB>, 7 | call: &HEVMCalls, 8 | ) -> Option> { 9 | if let HEVMCalls::Assume(inner) = call { 10 | Some(if inner.0 { Ok(Bytes::new()) } else { Err(ASSUME_MAGIC_RETURN_CODE.into()) }) 11 | } else { 12 | None 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /evm/src/executor/inspector/cheatcodes/util.rs: -------------------------------------------------------------------------------- 1 | use crate::abi::HEVMCalls; 2 | use bytes::Bytes; 3 | use ethers::{ 4 | abi::AbiEncode, 5 | prelude::{k256::ecdsa::SigningKey, LocalWallet, Signer}, 6 | types::{H256, U256}, 7 | utils, 8 | }; 9 | use revm::{Database, EVMData}; 10 | 11 | use super::Cheatcodes; 12 | 13 | fn addr(private_key: U256) -> Result { 14 | if private_key.is_zero() { 15 | return Err("Private key cannot be 0.".to_string().encode().into()) 16 | } 17 | 18 | let mut bytes: [u8; 32] = [0; 32]; 19 | private_key.to_big_endian(&mut bytes); 20 | 21 | let key = SigningKey::from_bytes(&bytes).map_err(|err| err.to_string().encode())?; 22 | let addr = utils::secret_key_to_address(&key); 23 | Ok(addr.encode().into()) 24 | } 25 | 26 | fn sign(private_key: U256, digest: H256, chain_id: U256) -> Result { 27 | if private_key.is_zero() { 28 | return Err("Private key cannot be 0.".to_string().encode().into()) 29 | } 30 | 31 | let mut bytes: [u8; 32] = [0; 32]; 32 | private_key.to_big_endian(&mut bytes); 33 | 34 | let key = SigningKey::from_bytes(&bytes).map_err(|err| err.to_string().encode())?; 35 | let wallet = LocalWallet::from(key).with_chain_id(chain_id.as_u64()); 36 | 37 | // The `ecrecover` precompile does not use EIP-155 38 | let sig = wallet.sign_hash(digest, false); 39 | let recovered = sig.recover(digest).map_err(|err| err.to_string().encode())?; 40 | 41 | assert_eq!(recovered, wallet.address()); 42 | 43 | let mut r_bytes = [0u8; 32]; 44 | let mut s_bytes = [0u8; 32]; 45 | sig.r.to_big_endian(&mut r_bytes); 46 | sig.s.to_big_endian(&mut s_bytes); 47 | 48 | Ok((sig.v, r_bytes, s_bytes).encode().into()) 49 | } 50 | 51 | pub fn apply( 52 | state: &mut Cheatcodes, 53 | data: &mut EVMData<'_, DB>, 54 | call: &HEVMCalls, 55 | ) -> Option> { 56 | Some(match call { 57 | HEVMCalls::Addr(inner) => addr(inner.0), 58 | HEVMCalls::Sign(inner) => sign(inner.0, inner.1.into(), data.env.cfg.chain_id), 59 | HEVMCalls::Label(inner) => { 60 | state.labels.insert(inner.0, inner.1.clone()); 61 | Ok(Bytes::new()) 62 | } 63 | _ => return None, 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /evm/src/executor/inspector/logs.rs: -------------------------------------------------------------------------------- 1 | use crate::executor::{ 2 | patch_hardhat_console_selector, HardhatConsoleCalls, HARDHAT_CONSOLE_ADDRESS, 3 | }; 4 | use bytes::Bytes; 5 | use ethers::{ 6 | abi::{AbiDecode, RawLog, Token}, 7 | types::{Address, H256}, 8 | }; 9 | use revm::{db::Database, CallInputs, EVMData, Gas, Inspector, Return}; 10 | 11 | /// An inspector that collects logs during execution. 12 | /// 13 | /// The inspector collects logs from the LOG opcodes as well as Hardhat-style logs. 14 | #[derive(Default)] 15 | pub struct LogCollector { 16 | pub logs: Vec, 17 | } 18 | 19 | impl LogCollector { 20 | fn hardhat_log(&mut self, input: Vec) -> (Return, Bytes) { 21 | // Patch the Hardhat-style selectors 22 | let input = patch_hardhat_console_selector(input.to_vec()); 23 | let decoded = match HardhatConsoleCalls::decode(&input) { 24 | Ok(inner) => inner, 25 | Err(err) => { 26 | return ( 27 | Return::Revert, 28 | ethers::abi::encode(&[Token::String(err.to_string())]).into(), 29 | ) 30 | } 31 | }; 32 | 33 | // Convert it to a DS-style `emit log(string)` event 34 | self.logs.push(convert_hh_log_to_event(decoded)); 35 | 36 | (Return::Continue, Bytes::new()) 37 | } 38 | } 39 | 40 | impl Inspector for LogCollector 41 | where 42 | DB: Database, 43 | { 44 | fn log(&mut self, _: &mut EVMData<'_, DB>, _: &Address, topics: &[H256], data: &Bytes) { 45 | self.logs.push(RawLog { topics: topics.to_vec(), data: data.to_vec() }); 46 | } 47 | 48 | fn call( 49 | &mut self, 50 | _: &mut EVMData<'_, DB>, 51 | call: &mut CallInputs, 52 | _: bool, 53 | ) -> (Return, Gas, Bytes) { 54 | if call.contract == HARDHAT_CONSOLE_ADDRESS { 55 | let (status, reason) = self.hardhat_log(call.input.to_vec()); 56 | (status, Gas::new(call.gas_limit), reason) 57 | } else { 58 | (Return::Continue, Gas::new(call.gas_limit), Bytes::new()) 59 | } 60 | } 61 | } 62 | 63 | /// Converts a call to Hardhat's `console.log` to a DSTest `log(string)` event. 64 | fn convert_hh_log_to_event(call: HardhatConsoleCalls) -> RawLog { 65 | RawLog { 66 | // This is topic 0 of DSTest's `log(string)` 67 | topics: vec![H256::from_slice( 68 | &hex::decode("41304facd9323d75b11bcdd609cb38effffdb05710f7caf0e9b16c6d9d709f50") 69 | .unwrap(), 70 | )], 71 | // Convert the parameters of the call to their string representation for the log 72 | data: ethers::abi::encode(&[Token::String(call.to_string())]), 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /evm/src/executor/inspector/mod.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | mod utils; 3 | 4 | mod logs; 5 | pub use logs::LogCollector; 6 | 7 | mod tracer; 8 | pub use tracer::Tracer; 9 | 10 | mod debugger; 11 | pub use debugger::Debugger; 12 | 13 | mod stack; 14 | pub use stack::{InspectorData, InspectorStack}; 15 | 16 | mod cheatcodes; 17 | pub use cheatcodes::Cheatcodes; 18 | 19 | use revm::BlockEnv; 20 | 21 | #[derive(Default, Clone, Debug)] 22 | pub struct InspectorStackConfig { 23 | /// The cheatcode inspector and its state, if cheatcodes are enabled. 24 | /// Whether or not cheatcodes are enabled 25 | pub cheatcodes: Option, 26 | /// The block environment 27 | /// 28 | /// Used in the cheatcode handler to overwrite the block environment separately from the 29 | /// execution block environment. 30 | pub block: BlockEnv, 31 | /// Whether or not tracing is enabled 32 | pub tracing: bool, 33 | /// Whether or not the debugger is enabled 34 | pub debugger: bool, 35 | } 36 | 37 | impl InspectorStackConfig { 38 | pub fn stack(&self) -> InspectorStack { 39 | let mut stack = 40 | InspectorStack { logs: Some(LogCollector::default()), ..Default::default() }; 41 | 42 | stack.cheatcodes = self.cheatcodes.clone(); 43 | if let Some(ref mut cheatcodes) = stack.cheatcodes { 44 | cheatcodes.block = Some(self.block.clone()); 45 | } 46 | 47 | if self.tracing { 48 | stack.tracer = Some(Tracer::default()); 49 | } 50 | if self.debugger { 51 | stack.debugger = Some(Debugger::default()); 52 | } 53 | stack 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /evm/src/executor/inspector/tracer.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | executor::{ 3 | inspector::utils::{gas_used, get_create_address}, 4 | HARDHAT_CONSOLE_ADDRESS, 5 | }, 6 | trace::{ 7 | CallTrace, CallTraceArena, LogCallOrder, RawOrDecodedCall, RawOrDecodedLog, 8 | RawOrDecodedReturnData, 9 | }, 10 | CallKind, 11 | }; 12 | use bytes::Bytes; 13 | use ethers::{ 14 | abi::RawLog, 15 | types::{Address, H256, U256}, 16 | }; 17 | use revm::{return_ok, CallInputs, CreateInputs, Database, EVMData, Gas, Inspector, Return}; 18 | 19 | /// An inspector that collects call traces. 20 | #[derive(Default, Debug)] 21 | pub struct Tracer { 22 | pub trace_stack: Vec, 23 | pub traces: CallTraceArena, 24 | } 25 | 26 | impl Tracer { 27 | pub fn start_trace( 28 | &mut self, 29 | depth: usize, 30 | address: Address, 31 | data: Vec, 32 | value: U256, 33 | kind: CallKind, 34 | ) { 35 | self.trace_stack.push(self.traces.push_trace( 36 | 0, 37 | CallTrace { 38 | depth, 39 | address, 40 | kind, 41 | data: RawOrDecodedCall::Raw(data), 42 | value, 43 | ..Default::default() 44 | }, 45 | )); 46 | } 47 | 48 | pub fn fill_trace( 49 | &mut self, 50 | success: bool, 51 | cost: u64, 52 | output: Vec, 53 | address: Option
, 54 | ) { 55 | let trace = &mut self.traces.arena 56 | [self.trace_stack.pop().expect("more traces were filled than started")] 57 | .trace; 58 | trace.success = success; 59 | trace.gas_cost = cost; 60 | trace.output = RawOrDecodedReturnData::Raw(output); 61 | 62 | if let Some(address) = address { 63 | trace.address = address; 64 | } 65 | } 66 | } 67 | 68 | impl Inspector for Tracer 69 | where 70 | DB: Database, 71 | { 72 | fn call( 73 | &mut self, 74 | data: &mut EVMData<'_, DB>, 75 | call: &mut CallInputs, 76 | _: bool, 77 | ) -> (Return, Gas, Bytes) { 78 | if call.contract != HARDHAT_CONSOLE_ADDRESS { 79 | self.start_trace( 80 | data.subroutine.depth() as usize, 81 | call.context.code_address, 82 | call.input.to_vec(), 83 | call.transfer.value, 84 | call.context.scheme.into(), 85 | ); 86 | } 87 | 88 | (Return::Continue, Gas::new(call.gas_limit), Bytes::new()) 89 | } 90 | 91 | fn log(&mut self, _: &mut EVMData<'_, DB>, _: &Address, topics: &[H256], data: &Bytes) { 92 | let node = &mut self.traces.arena[*self.trace_stack.last().expect("no ongoing trace")]; 93 | node.ordering.push(LogCallOrder::Log(node.logs.len())); 94 | node.logs 95 | .push(RawOrDecodedLog::Raw(RawLog { topics: topics.to_vec(), data: data.to_vec() })); 96 | } 97 | 98 | fn call_end( 99 | &mut self, 100 | data: &mut EVMData<'_, DB>, 101 | call: &CallInputs, 102 | gas: Gas, 103 | status: Return, 104 | retdata: Bytes, 105 | _: bool, 106 | ) -> (Return, Gas, Bytes) { 107 | if call.contract != HARDHAT_CONSOLE_ADDRESS { 108 | self.fill_trace( 109 | matches!(status, return_ok!()), 110 | gas_used(data.env.cfg.spec_id, gas.spend(), gas.refunded() as u64), 111 | retdata.to_vec(), 112 | None, 113 | ); 114 | } 115 | 116 | (status, gas, retdata) 117 | } 118 | 119 | fn create( 120 | &mut self, 121 | data: &mut EVMData<'_, DB>, 122 | call: &mut CreateInputs, 123 | ) -> (Return, Option
, Gas, Bytes) { 124 | // TODO: Does this increase gas cost? 125 | data.subroutine.load_account(call.caller, data.db); 126 | let nonce = data.subroutine.account(call.caller).info.nonce; 127 | self.start_trace( 128 | data.subroutine.depth() as usize, 129 | get_create_address(call, nonce), 130 | call.init_code.to_vec(), 131 | call.value, 132 | CallKind::Create, 133 | ); 134 | 135 | (Return::Continue, None, Gas::new(call.gas_limit), Bytes::new()) 136 | } 137 | 138 | fn create_end( 139 | &mut self, 140 | data: &mut EVMData<'_, DB>, 141 | _: &CreateInputs, 142 | status: Return, 143 | address: Option
, 144 | gas: Gas, 145 | retdata: Bytes, 146 | ) -> (Return, Option
, Gas, Bytes) { 147 | let code = match address { 148 | Some(address) => data 149 | .subroutine 150 | .account(address) 151 | .info 152 | .code 153 | .as_ref() 154 | .map_or(vec![], |code| code.to_vec()), 155 | None => vec![], 156 | }; 157 | self.fill_trace( 158 | matches!(status, return_ok!()), 159 | gas_used(data.env.cfg.spec_id, gas.spend(), gas.refunded() as u64), 160 | code, 161 | address, 162 | ); 163 | 164 | (status, address, gas, retdata) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /evm/src/executor/inspector/utils.rs: -------------------------------------------------------------------------------- 1 | use ethers::{ 2 | types::Address, 3 | utils::{get_contract_address, get_create2_address}, 4 | }; 5 | use revm::{CreateInputs, CreateScheme, SpecId}; 6 | 7 | /// Returns [Return::Continue] on an error, discarding the error. 8 | /// 9 | /// Useful for inspectors that read state that might be invalid, but do not want to emit 10 | /// appropriate errors themselves, instead opting to continue. 11 | macro_rules! try_or_continue { 12 | ($e:expr) => { 13 | match $e { 14 | Ok(v) => v, 15 | Err(_) => return Return::Continue, 16 | } 17 | }; 18 | } 19 | 20 | /// Get the address of a contract creation 21 | pub fn get_create_address(call: &CreateInputs, nonce: u64) -> Address { 22 | match call.scheme { 23 | CreateScheme::Create => get_contract_address(call.caller, nonce), 24 | CreateScheme::Create2 { salt } => { 25 | let mut buffer: [u8; 4 * 8] = [0; 4 * 8]; 26 | salt.to_big_endian(&mut buffer); 27 | get_create2_address(call.caller, buffer, call.init_code.clone()) 28 | } 29 | } 30 | } 31 | 32 | /// Get the gas used, accounting for refunds 33 | pub fn gas_used(spec: SpecId, spent: u64, refunded: u64) -> u64 { 34 | let refund_quotient = if SpecId::enabled(spec, SpecId::LONDON) { 5 } else { 2 }; 35 | spent - (refunded).min(spent / refund_quotient) 36 | } 37 | -------------------------------------------------------------------------------- /evm/src/fuzz/strategies/calldata.rs: -------------------------------------------------------------------------------- 1 | use super::fuzz_param; 2 | use ethers::{abi::Function, types::Bytes}; 3 | use proptest::prelude::{BoxedStrategy, Strategy}; 4 | 5 | /// Given a function, it returns a strategy which generates valid calldata 6 | /// for that function's input types. 7 | pub fn fuzz_calldata(func: Function) -> BoxedStrategy { 8 | // We need to compose all the strategies generated for each parameter in all 9 | // possible combinations 10 | let strats = func.inputs.iter().map(|input| fuzz_param(&input.kind)).collect::>(); 11 | 12 | strats 13 | .prop_map(move |tokens| { 14 | tracing::trace!(input = ?tokens); 15 | func.encode_input(&tokens).unwrap().into() 16 | }) 17 | .boxed() 18 | } 19 | -------------------------------------------------------------------------------- /evm/src/fuzz/strategies/mod.rs: -------------------------------------------------------------------------------- 1 | mod uint; 2 | pub use uint::UintStrategy; 3 | 4 | mod param; 5 | pub use param::{fuzz_param, fuzz_param_from_state}; 6 | 7 | mod calldata; 8 | pub use calldata::fuzz_calldata; 9 | 10 | mod state; 11 | pub use state::{ 12 | build_initial_state, collect_state_from_call, fuzz_calldata_from_state, EvmFuzzState, 13 | }; 14 | -------------------------------------------------------------------------------- /evm/src/fuzz/strategies/uint.rs: -------------------------------------------------------------------------------- 1 | use ethers::core::rand::Rng; 2 | use proptest::{ 3 | strategy::{NewTree, Strategy, ValueTree}, 4 | test_runner::TestRunner, 5 | }; 6 | 7 | use ethers::types::U256; 8 | 9 | /// Value tree for unsigned ints (up to uint256). 10 | /// This is very similar to [proptest::BinarySearch] 11 | pub struct UintValueTree { 12 | /// Lower base 13 | lo: U256, 14 | /// Current value 15 | curr: U256, 16 | /// Higher base 17 | hi: U256, 18 | /// If true cannot be simplified or complexified 19 | fixed: bool, 20 | } 21 | 22 | impl UintValueTree { 23 | /// Create a new tree 24 | /// # Arguments 25 | /// * `start` - Starting value for the tree 26 | /// * `fixed` - If `true` the tree would only contain one element and won't be simplified. 27 | fn new(start: U256, fixed: bool) -> Self { 28 | Self { lo: 0.into(), curr: start, hi: start, fixed } 29 | } 30 | 31 | fn reposition(&mut self) -> bool { 32 | let interval = self.hi - self.lo; 33 | let new_mid = self.lo + interval / 2; 34 | 35 | if new_mid == self.curr { 36 | false 37 | } else { 38 | self.curr = new_mid; 39 | true 40 | } 41 | } 42 | } 43 | 44 | impl ValueTree for UintValueTree { 45 | type Value = U256; 46 | 47 | fn current(&self) -> Self::Value { 48 | self.curr 49 | } 50 | 51 | fn simplify(&mut self) -> bool { 52 | if self.fixed || (self.hi <= self.lo) { 53 | return false 54 | } 55 | 56 | self.hi = self.curr; 57 | self.reposition() 58 | } 59 | 60 | fn complicate(&mut self) -> bool { 61 | if self.fixed || (self.hi <= self.lo) { 62 | return false 63 | } 64 | 65 | self.lo = self.curr + 1; 66 | self.reposition() 67 | } 68 | } 69 | 70 | /// Value tree for unsigned ints (up to uint256). 71 | /// The strategy combines 3 different strategies, each assigned a specific weight: 72 | /// 1. Generate purely random value in a range. This will first choose bit size uniformly (up `bits` 73 | /// param). Then generate a value for this bit size. 74 | /// 2. Generate a random value around the edges (+/- 3 around 0 and max possible value) 75 | /// 3. Generate a value from a predefined fixtures set 76 | #[derive(Debug)] 77 | pub struct UintStrategy { 78 | /// Bit sise of uint (e.g. 256) 79 | bits: usize, 80 | /// A set of fixtures to be generated 81 | fixtures: Vec, 82 | /// The weight for edge cases (+/- 3 around 0 and max possible value) 83 | edge_weight: usize, 84 | /// The weight for fixtures 85 | fixtures_weight: usize, 86 | /// The weight for purely random values 87 | random_weight: usize, 88 | } 89 | 90 | impl UintStrategy { 91 | /// Create a new strategy. 92 | /// #Arguments 93 | /// * `bits` - Size of uint in bits 94 | /// * `fixtures` - A set of fixed values to be generated (according to fixtures weight) 95 | pub fn new(bits: usize, fixtures: Vec) -> Self { 96 | Self { 97 | bits, 98 | fixtures, 99 | edge_weight: 10usize, 100 | fixtures_weight: 40usize, 101 | random_weight: 50usize, 102 | } 103 | } 104 | 105 | fn generate_edge_tree(&self, runner: &mut TestRunner) -> NewTree { 106 | let rng = runner.rng(); 107 | 108 | // Choose if we want values around 0 or max 109 | let is_min = rng.gen_bool(0.5); 110 | let offset = U256::from(rng.gen_range(0..4)); 111 | let max = if self.bits < 256 { 112 | (U256::from(1u8) << U256::from(self.bits)) - 1 113 | } else { 114 | U256::MAX 115 | }; 116 | let start = if is_min { offset } else { max - offset }; 117 | 118 | Ok(UintValueTree::new(start, false)) 119 | } 120 | 121 | fn generate_fixtures_tree(&self, runner: &mut TestRunner) -> NewTree { 122 | // generate edge cases if there's no fixtures 123 | if self.fixtures.is_empty() { 124 | return self.generate_edge_tree(runner) 125 | } 126 | let idx = runner.rng().gen_range(0..self.fixtures.len()); 127 | 128 | Ok(UintValueTree::new(self.fixtures[idx], false)) 129 | } 130 | 131 | fn generate_random_tree(&self, runner: &mut TestRunner) -> NewTree { 132 | let rng = runner.rng(); 133 | // generate random number of bits uniformly 134 | let bits = rng.gen_range(0..=self.bits); 135 | 136 | // init 2 128-bit randoms 137 | let mut higher: u128 = rng.gen_range(0..=u128::MAX); 138 | let mut lower: u128 = rng.gen_range(0..=u128::MAX); 139 | 140 | // cut 2 randoms according to bits size 141 | match bits { 142 | x if x < 128 => { 143 | lower &= (1u128 << x) - 1; 144 | higher = 0; 145 | } 146 | x if (128..256).contains(&x) => higher &= (1u128 << (x - 128)) - 1, 147 | _ => {} 148 | }; 149 | 150 | // init U256 from 2 randoms 151 | let mut inner: [u64; 4] = [0; 4]; 152 | let mask64 = (1 << 65) - 1; 153 | inner[0] = (lower & mask64) as u64; 154 | inner[1] = (lower >> 64) as u64; 155 | inner[2] = (higher & mask64) as u64; 156 | inner[3] = (higher >> 64) as u64; 157 | let start: U256 = U256(inner); 158 | 159 | Ok(UintValueTree::new(start, false)) 160 | } 161 | } 162 | 163 | impl Strategy for UintStrategy { 164 | type Tree = UintValueTree; 165 | type Value = U256; 166 | 167 | fn new_tree(&self, runner: &mut TestRunner) -> NewTree { 168 | let total_weight = self.random_weight + self.fixtures_weight + self.edge_weight; 169 | let bias = runner.rng().gen_range(0..total_weight); 170 | // randomly selecty one of 3 strategies 171 | match bias { 172 | x if x < self.edge_weight => self.generate_edge_tree(runner), 173 | x if x < self.edge_weight + self.fixtures_weight => self.generate_fixtures_tree(runner), 174 | _ => self.generate_random_tree(runner), 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /evm/src/lib.rs: -------------------------------------------------------------------------------- 1 | /// Decoding helpers 2 | pub mod decode; 3 | 4 | /// Call trace arena, decoding and formatting 5 | pub mod trace; 6 | 7 | /// Debugger data structures 8 | pub mod debug; 9 | 10 | /// Forge test execution backends 11 | pub mod executor; 12 | pub use executor::abi; 13 | 14 | /// Fuzzing wrapper for executors 15 | pub mod fuzz; 16 | 17 | // Re-exports 18 | pub use ethers::types::Address; 19 | pub use hashbrown::HashMap; 20 | 21 | use once_cell::sync::Lazy; 22 | pub static CALLER: Lazy
= Lazy::new(Address::random); 23 | 24 | use revm::{CallScheme, CreateScheme}; 25 | use serde::{Deserialize, Serialize}; 26 | 27 | #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] 28 | pub enum CallKind { 29 | Call, 30 | StaticCall, 31 | CallCode, 32 | DelegateCall, 33 | Create, 34 | } 35 | 36 | impl Default for CallKind { 37 | fn default() -> Self { 38 | CallKind::Call 39 | } 40 | } 41 | 42 | impl From for CallKind { 43 | fn from(scheme: CallScheme) -> Self { 44 | match scheme { 45 | CallScheme::Call => CallKind::Call, 46 | CallScheme::StaticCall => CallKind::StaticCall, 47 | CallScheme::CallCode => CallKind::CallCode, 48 | CallScheme::DelegateCall => CallKind::DelegateCall, 49 | } 50 | } 51 | } 52 | 53 | impl From for CallKind { 54 | fn from(_: CreateScheme) -> Self { 55 | CallKind::Create 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /evm/src/trace/identifier/local.rs: -------------------------------------------------------------------------------- 1 | use super::{AddressIdentity, TraceIdentifier}; 2 | use ethers::{ 3 | abi::{Abi, Address, Event}, 4 | prelude::ArtifactId, 5 | }; 6 | use std::{borrow::Cow, collections::BTreeMap}; 7 | 8 | /// A trace identifier that tries to identify addresses using local contracts. 9 | pub struct LocalTraceIdentifier { 10 | local_contracts: BTreeMap, (String, Abi)>, 11 | } 12 | 13 | impl LocalTraceIdentifier { 14 | pub fn new(known_contracts: &BTreeMap)>) -> Self { 15 | Self { 16 | local_contracts: known_contracts 17 | .iter() 18 | .map(|(id, (abi, runtime_code))| { 19 | (runtime_code.clone(), (id.name.clone(), abi.clone())) 20 | }) 21 | .collect(), 22 | } 23 | } 24 | 25 | /// Get all the events of the local contracts. 26 | pub fn events(&self) -> Vec { 27 | self.local_contracts.iter().flat_map(|(_, (_, abi))| abi.events().cloned()).collect() 28 | } 29 | } 30 | 31 | impl TraceIdentifier for LocalTraceIdentifier { 32 | fn identify_addresses( 33 | &self, 34 | addresses: Vec<(&Address, Option<&Vec>)>, 35 | ) -> Vec { 36 | addresses 37 | .into_iter() 38 | .filter_map(|(address, code)| { 39 | let code = code?; 40 | let (_, (name, abi)) = self 41 | .local_contracts 42 | .iter() 43 | .find(|(known_code, _)| diff_score(known_code, code) < 0.1)?; 44 | 45 | Some(AddressIdentity { 46 | address: *address, 47 | contract: Some(name.clone()), 48 | label: Some(name.clone()), 49 | abi: Some(Cow::Borrowed(abi)), 50 | }) 51 | }) 52 | .collect() 53 | } 54 | } 55 | 56 | /// Very simple fuzzy matching of contract bytecode. 57 | /// 58 | /// Will fail for small contracts that are essentially all immutable variables. 59 | fn diff_score(a: &[u8], b: &[u8]) -> f64 { 60 | let cutoff_len = usize::min(a.len(), b.len()); 61 | if cutoff_len == 0 { 62 | return 1.0 63 | } 64 | 65 | let a = &a[..cutoff_len]; 66 | let b = &b[..cutoff_len]; 67 | let mut diff_chars = 0; 68 | for i in 0..cutoff_len { 69 | if a[i] != b[i] { 70 | diff_chars += 1; 71 | } 72 | } 73 | diff_chars as f64 / cutoff_len as f64 74 | } 75 | -------------------------------------------------------------------------------- /evm/src/trace/identifier/mod.rs: -------------------------------------------------------------------------------- 1 | mod local; 2 | pub use local::LocalTraceIdentifier; 3 | 4 | mod etherscan; 5 | pub use etherscan::EtherscanIdentifier; 6 | 7 | use ethers::abi::{Abi, Address}; 8 | use std::borrow::Cow; 9 | 10 | /// An address identity 11 | pub struct AddressIdentity<'a> { 12 | /// The address this identity belongs to 13 | pub address: Address, 14 | /// The label for the address 15 | pub label: Option, 16 | /// The contract this address represents 17 | /// 18 | /// Note: This may be in the format `":"`. 19 | pub contract: Option, 20 | /// The ABI of the contract at this address 21 | pub abi: Option>, 22 | } 23 | 24 | /// Trace identifiers figure out what ABIs and labels belong to all the addresses of the trace. 25 | pub trait TraceIdentifier { 26 | // TODO: Update docs 27 | /// Attempts to identify an address in one or more call traces. 28 | #[allow(clippy::type_complexity)] 29 | fn identify_addresses( 30 | &self, 31 | addresses: Vec<(&Address, Option<&Vec>)>, 32 | ) -> Vec; 33 | } 34 | -------------------------------------------------------------------------------- /evm/src/trace/node.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | executor::CHEATCODE_ADDRESS, 3 | trace::{ 4 | utils, CallTrace, LogCallOrder, RawOrDecodedCall, RawOrDecodedLog, RawOrDecodedReturnData, 5 | }, 6 | }; 7 | use ethers::{ 8 | abi::{Abi, Function}, 9 | types::Address, 10 | }; 11 | use serde::{Deserialize, Serialize}; 12 | use std::collections::HashMap; 13 | 14 | /// A node in the arena 15 | #[derive(Default, Debug, Clone, Serialize, Deserialize)] 16 | pub struct CallTraceNode { 17 | /// Parent node index in the arena 18 | pub parent: Option, 19 | /// Children node indexes in the arena 20 | pub children: Vec, 21 | /// This node's index in the arena 22 | pub idx: usize, 23 | /// The call trace 24 | pub trace: CallTrace, 25 | /// Logs 26 | #[serde(skip)] 27 | pub logs: Vec, 28 | /// Ordering of child calls and logs 29 | pub ordering: Vec, 30 | } 31 | 32 | impl CallTraceNode { 33 | /// Decode a regular function 34 | pub fn decode_function( 35 | &mut self, 36 | funcs: &[Function], 37 | labels: &HashMap, 38 | errors: &Abi, 39 | ) { 40 | debug_assert!(!funcs.is_empty(), "requires at least 1 func"); 41 | // This is safe because (1) we would not have an entry for the given 42 | // selector if no functions with that selector were added and (2) the 43 | // same selector implies the function has 44 | // the same name and inputs. 45 | let func = &funcs[0]; 46 | 47 | if let RawOrDecodedCall::Raw(ref bytes) = self.trace.data { 48 | let inputs = if !bytes[4..].is_empty() { 49 | if self.trace.address == CHEATCODE_ADDRESS { 50 | // Try to decode cheatcode inputs in a more custom way 51 | utils::decode_cheatcode_inputs(func, bytes, errors).unwrap_or_else(|| { 52 | func.decode_input(&bytes[4..]) 53 | .expect("bad function input decode") 54 | .iter() 55 | .map(|token| utils::label(token, labels)) 56 | .collect() 57 | }) 58 | } else { 59 | match func.decode_input(&bytes[4..]) { 60 | Ok(v) => v.iter().map(|token| utils::label(token, labels)).collect(), 61 | Err(_) => Vec::new(), 62 | } 63 | } 64 | } else { 65 | Vec::new() 66 | }; 67 | self.trace.data = RawOrDecodedCall::Decoded(func.name.clone(), inputs); 68 | 69 | if let RawOrDecodedReturnData::Raw(bytes) = &self.trace.output { 70 | if !bytes.is_empty() { 71 | if self.trace.success { 72 | if let Some(tokens) = 73 | funcs.iter().find_map(|func| func.decode_output(&bytes[..]).ok()) 74 | { 75 | self.trace.output = RawOrDecodedReturnData::Decoded( 76 | tokens 77 | .iter() 78 | .map(|token| utils::label(token, labels)) 79 | .collect::>() 80 | .join(", "), 81 | ); 82 | } 83 | } else if let Ok(decoded_error) = 84 | foundry_utils::decode_revert(&bytes[..], Some(errors)) 85 | { 86 | self.trace.output = 87 | RawOrDecodedReturnData::Decoded(format!(r#""{}""#, decoded_error)); 88 | } 89 | } 90 | } 91 | } 92 | } 93 | 94 | /// Decode the node's tracing data for the given precompile function 95 | pub fn decode_precompile( 96 | &mut self, 97 | precompile_fn: &Function, 98 | labels: &HashMap, 99 | ) { 100 | if let RawOrDecodedCall::Raw(ref bytes) = self.trace.data { 101 | self.trace.label = Some("PRECOMPILE".to_string()); 102 | self.trace.data = RawOrDecodedCall::Decoded( 103 | precompile_fn.name.clone(), 104 | precompile_fn.decode_input(&bytes[..]).map_or_else( 105 | |_| vec![hex::encode(&bytes)], 106 | |tokens| tokens.iter().map(|token| utils::label(token, labels)).collect(), 107 | ), 108 | ); 109 | 110 | if let RawOrDecodedReturnData::Raw(ref bytes) = self.trace.output { 111 | self.trace.output = RawOrDecodedReturnData::Decoded( 112 | precompile_fn.decode_output(&bytes[..]).map_or_else( 113 | |_| hex::encode(&bytes), 114 | |tokens| { 115 | tokens 116 | .iter() 117 | .map(|token| utils::label(token, labels)) 118 | .collect::>() 119 | .join(", ") 120 | }, 121 | ), 122 | ); 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /evm/src/trace/utils.rs: -------------------------------------------------------------------------------- 1 | //! utilities used withing tracing 2 | 3 | use ethers::abi::{Abi, Address, Function, Token}; 4 | use foundry_utils::format_token; 5 | use std::collections::HashMap; 6 | 7 | /// Returns the label for the given `token` 8 | /// 9 | /// If the `token` is an `Address` then we look abel the label map. 10 | /// by default the token is formatted using standard formatting 11 | pub fn label(token: &Token, labels: &HashMap) -> String { 12 | match token { 13 | Token::Address(addr) => { 14 | if let Some(label) = labels.get(addr) { 15 | format!("{}: [{:?}]", label, addr) 16 | } else { 17 | format_token(token) 18 | } 19 | } 20 | _ => format_token(token), 21 | } 22 | } 23 | 24 | pub(crate) fn decode_cheatcode_inputs( 25 | func: &Function, 26 | data: &[u8], 27 | errors: &Abi, 28 | ) -> Option> { 29 | match func.name.as_str() { 30 | "expectRevert" => { 31 | foundry_utils::decode_revert(data, Some(errors)).ok().map(|decoded| vec![decoded]) 32 | } 33 | _ => None, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /evm/test-data/storage.json: -------------------------------------------------------------------------------- 1 | {"meta":{"cfg_env":{"chain_id":"0x1","spec_id":"LATEST","perf_all_precompiles_have_balance":false,"memory_limit":4294967295},"block_env":{"number":"0xdc42b8","coinbase":"0x0000000000000000000000000000000000000000","timestamp":"0x1","difficulty":"0x0","basefee":"0x0","gas_limit":"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"},"hosts":["mainnet.infura.io"]},"accounts":{"0x63091244180ae240c87d1f528f5f269134cb07b3":{"balance":"0x0","code_hash":"0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470","code":null,"nonce":0}},"storage":{"0x63091244180ae240c87d1f528f5f269134cb07b3":{"0x0":"0x0","0x1":"0x0","0x2":"0x0","0x3":"0x0","0x4":"0x0","0x5":"0x0","0x6":"0x0","0x7":"0x0","0x8":"0x0","0x9":"0x0"}},"block_hashes":{}} -------------------------------------------------------------------------------- /fmt/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "forge-fmt" 3 | version = "0.2.0" 4 | edition = "2021" 5 | description = """ 6 | Foundry's solidity formatting and linting support 7 | """ 8 | license = "MIT OR Apache-2.0" 9 | readme = "README.md" 10 | repository = "https://github.com/gakonst/foundry" 11 | keywords = ["ethereum", "web3", "solidity", "linter"] 12 | 13 | [dependencies] 14 | indent_write = "2.2.0" 15 | semver = "1.0.4" 16 | solang-parser = "0.1.12" 17 | 18 | [dev-dependencies] 19 | pretty_assertions = "1.0.0" 20 | itertools = "0.10.3" 21 | -------------------------------------------------------------------------------- /fmt/README.md: -------------------------------------------------------------------------------- 1 | # Formatter (`fmt`) 2 | 3 | Solidity formatter that respects (some parts of) the [Style Guide](https://docs.soliditylang.org/en/latest/style-guide.html) and 4 | is tested on the [Prettier Solidity Plugin](https://github.com/prettier-solidity/prettier-plugin-solidity) cases. 5 | 6 | ## Features (WIP) 7 | 8 | - [x] Pragma directive 9 | - [x] Import directive 10 | - [ ] Contract definition 11 | - [x] Enum definition 12 | - [ ] Struct definition 13 | - [ ] Event definition 14 | - [ ] Function definition 15 | - [ ] Function body 16 | - [ ] Variable definition 17 | - [ ] Comments 18 | 19 | ## Architecture 20 | 21 | The formatter works in two steps: 22 | 1. Parse Solidity source code with [solang](https://github.com/hyperledger-labs/solang) into the PT (Parse Tree) 23 | (not the same as Abstract Syntax Tree, [see difference](https://stackoverflow.com/a/9864571)). 24 | 2. Walk the PT and output new source code that's compliant with provided config and rule set. 25 | 26 | The technique for walking the tree is based on [Visitor Pattern](https://en.wikipedia.org/wiki/Visitor_pattern) 27 | and works as following: 28 | 1. Implement `Formatter` callback functions for each PT node type. 29 | Every callback function should write formatted output for the current node 30 | and call `Visitable::visit` function for child nodes delegating the output writing. 31 | 2. Implement `Visitable` trait and its `visit` function for each PT node type. Every `visit` function should call corresponding `Formatter`'s callback function. 32 | 33 | ### Example 34 | 35 | Source code 36 | ```solidity 37 | pragma solidity ^0.8.10 ; 38 | contract HelloWorld { 39 | string public message; 40 | constructor( string memory initMessage) { message = initMessage;} 41 | } 42 | 43 | 44 | event Greet( string indexed name) ; 45 | ``` 46 | 47 | Parse Tree (simplified) 48 | ```text 49 | SourceUnit 50 | | PragmaDirective("solidity", "^0.8.10") 51 | | ContractDefinition("HelloWorld") 52 | | VariableDefinition("string", "message", null, ["public"]) 53 | | FunctionDefinition("constructor") 54 | | Parameter("string", "initMessage", ["memory"]) 55 | | EventDefinition("string", "Greet", ["indexed"], ["name"]) 56 | ``` 57 | 58 | Formatted source code that was reconstructed from the Parse Tree 59 | ```solidity 60 | pragma solidity ^0.8.10; 61 | 62 | contract HelloWorld { 63 | string public message; 64 | 65 | constructor(string memory initMessage) { 66 | message = initMessage; 67 | } 68 | } 69 | 70 | event Greet(string indexed name); 71 | ``` 72 | -------------------------------------------------------------------------------- /fmt/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | mod formatter; 4 | mod loc; 5 | mod visit; 6 | 7 | pub use formatter::{Formatter, FormatterConfig}; 8 | pub use visit::Visitable; 9 | -------------------------------------------------------------------------------- /fmt/src/loc.rs: -------------------------------------------------------------------------------- 1 | use solang_parser::pt::{ 2 | CodeLocation, ContractPart, FunctionDefinition, Import, Loc, SourceUnitPart, YulExpression, 3 | YulStatement, 4 | }; 5 | 6 | pub trait LineOfCode { 7 | fn loc(&self) -> Loc; 8 | } 9 | 10 | impl LineOfCode for SourceUnitPart { 11 | fn loc(&self) -> Loc { 12 | match self { 13 | SourceUnitPart::ContractDefinition(contract) => contract.loc, 14 | SourceUnitPart::PragmaDirective(loc, _, _, _) | SourceUnitPart::StraySemicolon(loc) => { 15 | *loc 16 | } 17 | SourceUnitPart::ImportDirective(_, import) => *match import { 18 | Import::Plain(_, loc) => loc, 19 | Import::GlobalSymbol(_, _, loc) => loc, 20 | Import::Rename(_, _, loc) => loc, 21 | }, 22 | SourceUnitPart::EnumDefinition(enumeration) => enumeration.loc, 23 | SourceUnitPart::StructDefinition(structure) => structure.loc, 24 | SourceUnitPart::EventDefinition(event) => event.loc, 25 | SourceUnitPart::ErrorDefinition(error) => error.loc, 26 | SourceUnitPart::FunctionDefinition(function) => function.loc, 27 | SourceUnitPart::VariableDefinition(variable) => variable.loc, 28 | SourceUnitPart::TypeDefinition(def) => def.loc, 29 | } 30 | } 31 | } 32 | 33 | impl LineOfCode for ContractPart { 34 | fn loc(&self) -> Loc { 35 | match self { 36 | ContractPart::StructDefinition(structure) => structure.loc, 37 | ContractPart::EventDefinition(event) => event.loc, 38 | ContractPart::ErrorDefinition(error) => error.loc, 39 | ContractPart::EnumDefinition(enumeration) => enumeration.loc, 40 | ContractPart::VariableDefinition(variable) => variable.loc, 41 | ContractPart::FunctionDefinition(function) => function.loc(), 42 | ContractPart::StraySemicolon(loc) => *loc, 43 | ContractPart::Using(using) => using.loc, 44 | ContractPart::TypeDefinition(def) => def.loc, 45 | } 46 | } 47 | } 48 | 49 | impl LineOfCode for YulStatement { 50 | fn loc(&self) -> Loc { 51 | match self { 52 | YulStatement::Assign(loc, _, _) | 53 | YulStatement::If(loc, _, _) | 54 | YulStatement::Leave(loc) | 55 | YulStatement::Break(loc) | 56 | YulStatement::VariableDeclaration(loc, _, _) | 57 | YulStatement::Continue(loc) => *loc, 58 | YulStatement::For(f) => f.loc, 59 | YulStatement::Block(b) => b.loc, 60 | YulStatement::Switch(s) => s.loc, 61 | YulStatement::FunctionDefinition(f) => f.loc, 62 | YulStatement::FunctionCall(f) => f.loc, 63 | } 64 | } 65 | } 66 | 67 | impl LineOfCode for YulExpression { 68 | fn loc(&self) -> Loc { 69 | match self { 70 | YulExpression::BoolLiteral(loc, _, _) | 71 | YulExpression::NumberLiteral(loc, _, _) | 72 | YulExpression::HexNumberLiteral(loc, _, _) | 73 | YulExpression::Member(loc, _, _) => *loc, 74 | YulExpression::StringLiteral(literal, _) => literal.loc, 75 | YulExpression::Variable(ident) => ident.loc, 76 | YulExpression::FunctionCall(f) => f.loc, 77 | YulExpression::HexStringLiteral(lit, _) => lit.loc, 78 | } 79 | } 80 | } 81 | 82 | impl LineOfCode for FunctionDefinition { 83 | fn loc(&self) -> Loc { 84 | Loc::File( 85 | self.loc.file_no(), 86 | self.loc.start(), 87 | self.body.as_ref().map(|body| body.loc().end()).unwrap_or_else(|| self.loc.end()), 88 | ) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /fmt/testdata/ContractDefinitions/bracket-spacing.fmt.sol: -------------------------------------------------------------------------------- 1 | // config: line-length=160 2 | // config: bracket-spacing=true 3 | contract ContractDefinition is Contract1, Contract2, Contract3, Contract4, Contract5 { } 4 | -------------------------------------------------------------------------------- /fmt/testdata/ContractDefinitions/fmt.sol: -------------------------------------------------------------------------------- 1 | contract ContractDefinition is 2 | Contract1, 3 | Contract2, 4 | Contract3, 5 | Contract4, 6 | Contract5 7 | {} 8 | -------------------------------------------------------------------------------- /fmt/testdata/ContractDefinitions/original.sol: -------------------------------------------------------------------------------- 1 | contract ContractDefinition is Contract1, Contract2, Contract3, Contract4, Contract5 { 2 | } -------------------------------------------------------------------------------- /fmt/testdata/EnumDefinitions/bracket-spacing.fmt.sol: -------------------------------------------------------------------------------- 1 | // config: bracket-spacing=true 2 | contract EnumDefinitions { 3 | enum Empty { } 4 | enum ActionChoices { 5 | GoLeft, 6 | GoRight, 7 | GoStraight, 8 | SitStill 9 | } 10 | enum States { 11 | State1, 12 | State2, 13 | State3, 14 | State4, 15 | State5, 16 | State6, 17 | State7, 18 | State8, 19 | State9 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /fmt/testdata/EnumDefinitions/fmt.sol: -------------------------------------------------------------------------------- 1 | contract EnumDefinitions { 2 | enum Empty {} 3 | enum ActionChoices { 4 | GoLeft, 5 | GoRight, 6 | GoStraight, 7 | SitStill 8 | } 9 | enum States { 10 | State1, 11 | State2, 12 | State3, 13 | State4, 14 | State5, 15 | State6, 16 | State7, 17 | State8, 18 | State9 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /fmt/testdata/EnumDefinitions/original.sol: -------------------------------------------------------------------------------- 1 | contract EnumDefinitions { 2 | enum Empty { 3 | 4 | } 5 | enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill } 6 | enum States { State1, State2, State3, State4, State5, State6, State7, State8, State9 } 7 | } -------------------------------------------------------------------------------- /fmt/testdata/ImportDirective/bracket-spacing.fmt.sol: -------------------------------------------------------------------------------- 1 | // config: bracket-spacing=true 2 | import "SomeFile.sol"; 3 | import "SomeFile.sol" as SomeOtherFile; 4 | import "AnotherFile.sol" as SomeSymbol; 5 | import { symbol1 as alias, symbol2 } from "File.sol"; 6 | import { symbol1 as alias1, symbol2 as alias2, symbol3 as alias3, symbol4 } from "File2.sol"; 7 | -------------------------------------------------------------------------------- /fmt/testdata/ImportDirective/fmt.sol: -------------------------------------------------------------------------------- 1 | import "SomeFile.sol"; 2 | import "SomeFile.sol" as SomeOtherFile; 3 | import "AnotherFile.sol" as SomeSymbol; 4 | import {symbol1 as alias, symbol2} from "File.sol"; 5 | import {symbol1 as alias1, symbol2 as alias2, symbol3 as alias3, symbol4} from "File2.sol"; 6 | -------------------------------------------------------------------------------- /fmt/testdata/ImportDirective/original.sol: -------------------------------------------------------------------------------- 1 | import "SomeFile.sol"; 2 | import "SomeFile.sol" as SomeOtherFile; 3 | import * as SomeSymbol from "AnotherFile.sol"; 4 | import {symbol1 as alias, symbol2} from "File.sol"; 5 | import {symbol1 as alias1, symbol2 as alias2, symbol3 as alias3, symbol4} from "File2.sol"; 6 | -------------------------------------------------------------------------------- /fmt/testdata/StatementBlock/bracket-spacing.fmt.sol: -------------------------------------------------------------------------------- 1 | // config: bracket-spacing=true 2 | contract Contract { 3 | function test() { 4 | unchecked { a += 1; } 5 | 6 | unchecked { 7 | a += 1; 8 | } 9 | 2 + 2; 10 | 11 | unchecked { 12 | a += 1; 13 | } 14 | unchecked { } 15 | 16 | 1 + 1; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /fmt/testdata/StatementBlock/fmt.sol: -------------------------------------------------------------------------------- 1 | contract Contract { 2 | function test() { 3 | unchecked {a += 1;} 4 | 5 | unchecked { 6 | a += 1; 7 | } 8 | 2 + 2; 9 | 10 | unchecked { 11 | a += 1; 12 | } 13 | unchecked {} 14 | 15 | 1 + 1; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /fmt/testdata/StatementBlock/original.sol: -------------------------------------------------------------------------------- 1 | contract Contract { 2 | function test() { unchecked { a += 1; } 3 | 4 | unchecked { 5 | a += 1; 6 | } 7 | 2 + 2; 8 | 9 | unchecked { a += 1; 10 | } 11 | unchecked {} 12 | 13 | 1 + 1; 14 | 15 | 16 | } 17 | } -------------------------------------------------------------------------------- /fmt/testdata/StructDefinition/bracket-spacing.fmt.sol: -------------------------------------------------------------------------------- 1 | // config: bracket-spacing=true 2 | struct Foo { } 3 | 4 | struct Bar { 5 | uint256 foo; 6 | string bar; 7 | } 8 | -------------------------------------------------------------------------------- /fmt/testdata/StructDefinition/fmt.sol: -------------------------------------------------------------------------------- 1 | struct Foo {} 2 | 3 | struct Bar { 4 | uint256 foo; 5 | string bar; 6 | } 7 | -------------------------------------------------------------------------------- /fmt/testdata/StructDefinition/original.sol: -------------------------------------------------------------------------------- 1 | struct Foo { 2 | } struct Bar { uint foo ;string bar ; } -------------------------------------------------------------------------------- /fmt/testdata/TypeDefinition/fmt.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.8; 2 | 3 | type Hello is uint256; 4 | 5 | contract TypeDefinition { 6 | event Moon(Hello world); 7 | 8 | function demo(Hello world) public { 9 | world = Hello.wrap(Hello.unwrap(world) + 1337); 10 | emit Moon(world); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /fmt/testdata/TypeDefinition/original.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.8; 2 | 3 | type Hello is uint; 4 | 5 | contract TypeDefinition { 6 | event Moon(Hello world); 7 | 8 | function demo(Hello world) public { 9 | world = Hello.wrap(Hello.unwrap(world) + 1337); 10 | emit Moon(world); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /forge/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "forge" 3 | version = "0.2.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | 7 | [dependencies] 8 | foundry-utils = { path = "./../utils" } 9 | foundry-config = { path = "./../config" } 10 | foundry-evm = { path = "./../evm" } 11 | 12 | ethers = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["solc-full"] } 13 | eyre = "0.6.5" 14 | semver = "1.0.5" 15 | serde_json = "1.0.67" 16 | serde = "1.0.130" 17 | regex = { version = "1.5.4", default-features = false } 18 | hex = "0.4.3" 19 | glob = "0.3.0" 20 | # TODO: Trim down 21 | tokio = { version = "1.10.1" } 22 | tracing = "0.1.26" 23 | tracing-subscriber = "=0.3.9" 24 | proptest = "1.0.0" 25 | rayon = "1.5" 26 | rlp = "0.5.1" 27 | once_cell = "1.9.0" 28 | comfy-table = "5.0.0" 29 | 30 | [dev-dependencies] 31 | ethers = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["solc-full", "solc-tests"] } 32 | foundry-utils = { path = "./../utils", features = ["test"] } 33 | -------------------------------------------------------------------------------- /forge/src/lib.rs: -------------------------------------------------------------------------------- 1 | /// Gas reports 2 | pub mod gas_report; 3 | 4 | /// The Forge test runner 5 | mod runner; 6 | pub use runner::{ContractRunner, SuiteResult, TestKind, TestKindGas, TestResult}; 7 | 8 | /// Forge test runners for multiple contracts 9 | mod multi_runner; 10 | pub use multi_runner::{MultiContractRunner, MultiContractRunnerBuilder}; 11 | 12 | pub trait TestFilter { 13 | fn matches_test(&self, test_name: impl AsRef) -> bool; 14 | fn matches_contract(&self, contract_name: impl AsRef) -> bool; 15 | fn matches_path(&self, path: impl AsRef) -> bool; 16 | } 17 | 18 | /// The Forge EVM backend 19 | pub use foundry_evm::*; 20 | 21 | #[cfg(test)] 22 | pub mod test_helpers { 23 | use crate::TestFilter; 24 | use ethers::{ 25 | prelude::{artifacts::Settings, Lazy, ProjectCompileOutput, SolcConfig}, 26 | solc::{Project, ProjectPathsConfig}, 27 | types::{Address, U256}, 28 | }; 29 | use foundry_evm::{ 30 | executor::{ 31 | builder::Backend, 32 | opts::{Env, EvmOpts}, 33 | DatabaseRef, Executor, ExecutorBuilder, 34 | }, 35 | fuzz::FuzzedExecutor, 36 | CALLER, 37 | }; 38 | use foundry_utils::RuntimeOrHandle; 39 | use std::str::FromStr; 40 | 41 | pub static PROJECT: Lazy = Lazy::new(|| { 42 | let paths = ProjectPathsConfig::builder() 43 | .root("../testdata") 44 | .sources("../testdata") 45 | .build() 46 | .unwrap(); 47 | Project::builder().paths(paths).ephemeral().no_artifacts().build().unwrap() 48 | }); 49 | 50 | pub static LIBS_PROJECT: Lazy = Lazy::new(|| { 51 | let paths = ProjectPathsConfig::builder() 52 | .root("../testdata") 53 | .sources("../testdata") 54 | .build() 55 | .unwrap(); 56 | let libs = 57 | ["fork/Fork.t.sol:DssExecLib:0xfD88CeE74f7D78697775aBDAE53f9Da1559728E4".to_string()]; 58 | 59 | let settings = Settings { 60 | libraries: foundry_config::parse_libraries(&libs).unwrap(), 61 | ..Default::default() 62 | }; 63 | 64 | let solc_config = SolcConfig::builder().settings(settings).build(); 65 | Project::builder() 66 | .paths(paths) 67 | .ephemeral() 68 | .no_artifacts() 69 | .solc_config(solc_config) 70 | .build() 71 | .unwrap() 72 | }); 73 | 74 | pub static COMPILED: Lazy = Lazy::new(|| (*PROJECT).compile().unwrap()); 75 | 76 | pub static COMPILED_WITH_LIBS: Lazy = 77 | Lazy::new(|| (*LIBS_PROJECT).compile().unwrap()); 78 | 79 | pub static EVM_OPTS: Lazy = Lazy::new(|| EvmOpts { 80 | env: Env { 81 | gas_limit: 18446744073709551615, 82 | chain_id: Some(99), 83 | tx_origin: Address::from_str("00a329c0648769a73afac7f9381e08fb43dbea72").unwrap(), 84 | ..Default::default() 85 | }, 86 | sender: Address::from_str("00a329c0648769a73afac7f9381e08fb43dbea72").unwrap(), 87 | initial_balance: U256::MAX, 88 | ffi: true, 89 | memory_limit: 2u64.pow(24), 90 | ..Default::default() 91 | }); 92 | 93 | pub fn test_executor() -> Executor { 94 | let env = RuntimeOrHandle::new().block_on((*EVM_OPTS).evm_env()); 95 | ExecutorBuilder::new().with_cheatcodes(false).with_config(env).build(Backend::simple()) 96 | } 97 | 98 | pub fn fuzz_executor(executor: &Executor) -> FuzzedExecutor { 99 | let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() }; 100 | 101 | FuzzedExecutor::new(executor, proptest::test_runner::TestRunner::new(cfg), *CALLER) 102 | } 103 | 104 | pub mod filter { 105 | use super::*; 106 | use regex::Regex; 107 | 108 | pub struct Filter { 109 | test_regex: Regex, 110 | contract_regex: Regex, 111 | path_regex: Regex, 112 | } 113 | 114 | impl Filter { 115 | pub fn new(test_pattern: &str, contract_pattern: &str, path_pattern: &str) -> Self { 116 | Filter { 117 | test_regex: Regex::new(test_pattern).unwrap(), 118 | contract_regex: Regex::new(contract_pattern).unwrap(), 119 | path_regex: Regex::new(path_pattern).unwrap(), 120 | } 121 | } 122 | 123 | pub fn matches_all() -> Self { 124 | Filter { 125 | test_regex: Regex::new(".*").unwrap(), 126 | contract_regex: Regex::new(".*").unwrap(), 127 | path_regex: Regex::new(".*").unwrap(), 128 | } 129 | } 130 | } 131 | 132 | impl TestFilter for Filter { 133 | fn matches_test(&self, test_name: impl AsRef) -> bool { 134 | self.test_regex.is_match(test_name.as_ref()) 135 | } 136 | 137 | fn matches_contract(&self, contract_name: impl AsRef) -> bool { 138 | self.contract_regex.is_match(contract_name.as_ref()) 139 | } 140 | 141 | fn matches_path(&self, path: impl AsRef) -> bool { 142 | self.path_regex.is_match(path.as_ref()) 143 | } 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /foundryup/README.md: -------------------------------------------------------------------------------- 1 | # `foundryup` 2 | 3 | Update or revert to a specific Foundry branch with ease. 4 | 5 | ## Installing 6 | 7 | ```sh 8 | curl -L https://foundry.paradigm.xyz | bash 9 | ``` 10 | 11 | ## Usage 12 | 13 | To install the **nightly** version: 14 | 15 | ```sh 16 | foundryup 17 | ``` 18 | 19 | To install a specific **version** (in this case the `nightly` version): 20 | 21 | ```sh 22 | foundryup --version nightly 23 | ``` 24 | 25 | To install a specific **branch** (in this case the `release/0.1.0` branch's latest commit): 26 | 27 | ```sh 28 | foundryup --branch release/0.1.0 29 | ``` 30 | 31 | To install a **fork's main branch** (in this case `transmissions11/foundry`'s main branch): 32 | 33 | ```sh 34 | foundryup --repo transmissions11/foundry 35 | ``` 36 | 37 | To install a **specific branch in a fork** (in this case the `patch-10` branch's latest commit in `transmissions11/foundry`): 38 | 39 | ```sh 40 | foundryup --repo transmissions11/foundry --branch patch-10 41 | ``` 42 | 43 | To install from a **specific Pull Request**: 44 | 45 | ```sh 46 | foundryup --pr 1071 47 | ``` 48 | 49 | To install from a **specific commit**: 50 | ```sh 51 | foundryup -C 94bfdb2 52 | ``` 53 | 54 | To install a local directory or repository (e.g. one located at `~/git/foundry`, assuming you're in the home directory) 55 | ##### Note: --branch, --repo, and --version flags are ignored during local installations. 56 | 57 | ```sh 58 | foundryup --path ./git/foundry 59 | ``` 60 | 61 | --- 62 | 63 | **Tip**: All flags have a single character shorthand equivalent! You can use `-v` instead of `--version`, etc. 64 | 65 | --- 66 | -------------------------------------------------------------------------------- /foundryup/install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | echo Installing foundryup... 5 | 6 | FOUNDRY_DIR=${FOUNDRY_DIR-"$HOME/.foundry"} 7 | FOUNDRY_BIN_DIR="$FOUNDRY_DIR/bin" 8 | FOUNDRY_MAN_DIR="$FOUNDRY_DIR/share/man/man1" 9 | 10 | BIN_URL="https://raw.githubusercontent.com/foundry-rs/foundry/master/foundryup/foundryup" 11 | BIN_PATH="$FOUNDRY_BIN_DIR/foundryup" 12 | 13 | # Create the .foundry bin directory and foundryup binary if it doesn't exist. 14 | mkdir -p $FOUNDRY_BIN_DIR 15 | curl -# -L $BIN_URL -o $BIN_PATH 16 | chmod +x $BIN_PATH 17 | 18 | # Create the man directory for future man files if it doesn't exist. 19 | mkdir -p $FOUNDRY_MAN_DIR 20 | 21 | # Store the correct profile file (i.e. .profile for bash or .zshrc for ZSH). 22 | case $SHELL in 23 | */zsh) 24 | PROFILE=$HOME/.zshrc 25 | PREF_SHELL=zsh 26 | ;; 27 | */bash) 28 | PROFILE=$HOME/.bashrc 29 | PREF_SHELL=bash 30 | ;; 31 | */fish) 32 | PROFILE=$HOME/.config/fish/config.fish 33 | PREF_SHELL=fish 34 | ;; 35 | *) 36 | echo "foundryup: could not detect shell, manually add ${FOUNDRY_BIN_DIR} to your PATH." 37 | exit 1 38 | esac 39 | 40 | # Only add foundryup if it isn't already in PATH. 41 | if [[ ":$PATH:" != *":${FOUNDRY_BIN_DIR}:"* ]]; then 42 | # Add the foundryup directory to the path and ensure the old PATH variables remain. 43 | echo >> $PROFILE && echo "export PATH=\"\$PATH:$FOUNDRY_BIN_DIR\"" >> $PROFILE 44 | fi 45 | 46 | # Warn MacOS users that they may need to manually install libusb via Homebrew: 47 | if [[ "$OSTYPE" =~ ^darwin && ! -f /usr/local/opt/libusb/lib/libusb-1.0.0.dylib ]]; then 48 | echo && echo "warning: libusb not found. You may need to install it manually on MacOS via Homebrew (brew install libusb)." 49 | fi 50 | 51 | echo && echo "Detected your preferred shell is ${PREF_SHELL} and added foundryup to PATH. Run 'source ${PROFILE}' or start a new terminal session to use foundryup." 52 | echo "Then, simply run 'foundryup' to install Foundry." 53 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.59" 3 | profile = "default" 4 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | reorder_imports = true 2 | imports_granularity = "Crate" 3 | use_small_heuristics = "Max" 4 | comment_width = 100 5 | wrap_comments = true 6 | binop_separator = "Back" 7 | trailing_comma = "Vertical" 8 | trailing_semicolon = false 9 | use_field_init_shorthand = true -------------------------------------------------------------------------------- /testdata/README.md: -------------------------------------------------------------------------------- 1 | ## Foundry Tests 2 | 3 | A test suite that tests different aspects of Foundry. 4 | 5 | ### Structure 6 | 7 | - [`core`](core): Tests for fundamental aspects of Foundry 8 | - [`logs`](logs): Tests for Foundry logging capabilities 9 | - [`cheats`](cheats): Tests for Foundry cheatcodes 10 | - [`fuzz`](fuzz): Tests for the Foundry fuzzer 11 | - [`fuzz`](fuzz): Tests for Foundry tracer 12 | -------------------------------------------------------------------------------- /testdata/cheats/Addr.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "./Cheats.sol"; 6 | 7 | contract AddrTest is DSTest { 8 | Cheats constant cheats = Cheats(HEVM_ADDRESS); 9 | 10 | function testFailPrivKeyZero() public { 11 | cheats.addr(0); 12 | } 13 | 14 | function testAddr() public { 15 | uint pk = 77814517325470205911140941194401928579557062014761831930645393041380819009408; 16 | address expected = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; 17 | 18 | assertEq(cheats.addr(pk), expected, "expected address did not match"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /testdata/cheats/Assume.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "./Cheats.sol"; 6 | 7 | contract AssumeTest is DSTest { 8 | Cheats constant cheats = Cheats(HEVM_ADDRESS); 9 | 10 | function testAssume(uint8 x) public { 11 | cheats.assume(x < 2 ** 7); 12 | assertTrue(x < 2 ** 7, "did not discard inputs"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /testdata/cheats/Cheats.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | interface Cheats { 5 | // Set block.timestamp (newTimestamp) 6 | function warp(uint256) external; 7 | // Set block.height (newHeight) 8 | function roll(uint256) external; 9 | // Set block.basefee (newBasefee) 10 | function fee(uint256) external; 11 | // Loads a storage slot from an address (who, slot) 12 | function load(address,bytes32) external returns (bytes32); 13 | // Stores a value to an address' storage slot, (who, slot, value) 14 | function store(address,bytes32,bytes32) external; 15 | // Signs data, (privateKey, digest) => (v, r, s) 16 | function sign(uint256,bytes32) external returns (uint8,bytes32,bytes32); 17 | // Gets address for a given private key, (privateKey) => (address) 18 | function addr(uint256) external returns (address); 19 | // Performs a foreign function call via terminal, (stringInputs) => (result) 20 | function ffi(string[] calldata) external returns (bytes memory); 21 | // Sets the *next* call's msg.sender to be the input address 22 | function prank(address) external; 23 | // Sets all subsequent calls' msg.sender to be the input address until `stopPrank` is called 24 | function startPrank(address) external; 25 | // Sets the *next* call's msg.sender to be the input address, and the tx.origin to be the second input 26 | function prank(address,address) external; 27 | // Sets all subsequent calls' msg.sender to be the input address until `stopPrank` is called, and the tx.origin to be the second input 28 | function startPrank(address,address) external; 29 | // Resets subsequent calls' msg.sender to be `address(this)` 30 | function stopPrank() external; 31 | // Sets an address' balance, (who, newBalance) 32 | function deal(address, uint256) external; 33 | // Sets an address' code, (who, newCode) 34 | function etch(address, bytes calldata) external; 35 | // Expects an error on next call 36 | function expectRevert() external; 37 | function expectRevert(bytes calldata) external; 38 | function expectRevert(bytes4) external; 39 | // Record all storage reads and writes 40 | function record() external; 41 | // Gets all accessed reads and write slot from a recording session, for a given address 42 | function accesses(address) external returns (bytes32[] memory reads, bytes32[] memory writes); 43 | // Prepare an expected log with (bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData). 44 | // Call this function, then emit an event, then call a function. Internally after the call, we check if 45 | // logs were emitted in the expected order with the expected topics and data (as specified by the booleans) 46 | function expectEmit(bool,bool,bool,bool) external; 47 | // Mocks a call to an address, returning specified data. 48 | // Calldata can either be strict or a partial match, e.g. if you only 49 | // pass a Solidity selector to the expected calldata, then the entire Solidity 50 | // function will be mocked. 51 | function mockCall(address,bytes calldata,bytes calldata) external; 52 | // Clears all mocked calls 53 | function clearMockedCalls() external; 54 | // Expect a call to an address with the specified calldata. 55 | // Calldata can either be strict or a partial match 56 | function expectCall(address,bytes calldata) external; 57 | // Gets the code from an artifact file. Takes in the relative path to the json file 58 | function getCode(string calldata) external returns (bytes memory); 59 | // Labels an address in call traces 60 | function label(address, string calldata) external; 61 | // If the condition is false, discard this run's fuzz inputs and generate new ones 62 | function assume(bool) external; 63 | // Set nonce for an account 64 | function setNonce(address,uint64) external; 65 | // Get nonce for an account 66 | function getNonce(address) external returns(uint64); 67 | // Set block.chainid (newChainId) 68 | function chainId(uint256) external; 69 | } 70 | -------------------------------------------------------------------------------- /testdata/cheats/Deal.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "./Cheats.sol"; 6 | 7 | contract DealTest is DSTest { 8 | Cheats constant cheats = Cheats(HEVM_ADDRESS); 9 | 10 | function testDeal(uint256 amount) public { 11 | address target = address(1); 12 | assertEq(target.balance, 0, "initial balance incorrect"); 13 | 14 | // Give half the amount 15 | cheats.deal(target, amount / 2); 16 | assertEq(target.balance, amount / 2, "half balance is incorrect"); 17 | 18 | // Give the entire amount to check that deal is not additive 19 | cheats.deal(target, amount); 20 | assertEq(target.balance, amount, "deal did not overwrite balance"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /testdata/cheats/Etch.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "./Cheats.sol"; 6 | 7 | contract EtchTest is DSTest { 8 | Cheats constant cheats = Cheats(HEVM_ADDRESS); 9 | 10 | function testEtch() public { 11 | address target = address(10); 12 | bytes memory code = hex"1010"; 13 | cheats.etch(target, code); 14 | assertEq(string(code), string(target.code)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /testdata/cheats/ExpectCall.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "./Cheats.sol"; 6 | 7 | contract Contract { 8 | function numberA() public pure returns (uint256) { 9 | return 1; 10 | } 11 | 12 | function numberB() public pure returns (uint256) { 13 | return 2; 14 | } 15 | 16 | function add(uint256 a, uint256 b) public pure returns (uint256) { 17 | return a + b; 18 | } 19 | } 20 | 21 | contract NestedContract { 22 | Contract private inner; 23 | 24 | constructor(Contract _inner) { 25 | inner = _inner; 26 | } 27 | 28 | function sum() public view returns (uint256) { 29 | return inner.numberA() + inner.numberB(); 30 | } 31 | 32 | function hello() public pure returns (string memory) { 33 | return "hi"; 34 | } 35 | } 36 | 37 | contract ExpectCallTest is DSTest { 38 | Cheats constant cheats = Cheats(HEVM_ADDRESS); 39 | 40 | function testExpectCallWithData() public { 41 | Contract target = new Contract(); 42 | cheats.expectCall( 43 | address(target), 44 | abi.encodeWithSelector(target.add.selector, 1, 2) 45 | ); 46 | target.add(1, 2); 47 | } 48 | 49 | function testFailExpectCallWithData() public { 50 | Contract target = new Contract(); 51 | cheats.expectCall( 52 | address(target), 53 | abi.encodeWithSelector(target.add.selector, 1, 2) 54 | ); 55 | target.add(3, 3); 56 | } 57 | 58 | function testExpectInnerCall() public { 59 | Contract inner = new Contract(); 60 | NestedContract target = new NestedContract(inner); 61 | 62 | cheats.expectCall( 63 | address(inner), 64 | abi.encodeWithSelector(inner.numberB.selector) 65 | ); 66 | target.sum(); 67 | } 68 | 69 | function testFailExpectInnerCall() public { 70 | Contract inner = new Contract(); 71 | NestedContract target = new NestedContract(inner); 72 | 73 | cheats.expectCall( 74 | address(inner), 75 | abi.encodeWithSelector(inner.numberB.selector) 76 | ); 77 | 78 | // this function does not call inner 79 | target.hello(); 80 | } 81 | 82 | function testExpectSelectorCall() public { 83 | Contract target = new Contract(); 84 | cheats.expectCall( 85 | address(target), 86 | abi.encodeWithSelector(target.add.selector) 87 | ); 88 | target.add(5, 5); 89 | } 90 | 91 | function testFailExpectSelectorCall() public { 92 | Contract target = new Contract(); 93 | cheats.expectCall( 94 | address(target), 95 | abi.encodeWithSelector(target.add.selector) 96 | ); 97 | } 98 | 99 | function testFailExpectCallWithMoreParameters() public { 100 | Contract target = new Contract(); 101 | cheats.expectCall( 102 | address(target), 103 | abi.encodeWithSelector(target.add.selector, 3, 3, 3) 104 | ); 105 | target.add(3, 3); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /testdata/cheats/ExpectRevert.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "./Cheats.sol"; 6 | 7 | contract Reverter { 8 | error CustomError(); 9 | 10 | function revertWithMessage(string memory message) public pure { 11 | require(false, message); 12 | } 13 | 14 | function doNotRevert() public pure {} 15 | 16 | function panic() public pure returns (uint256) { 17 | return uint256(100) - uint256(101); 18 | } 19 | 20 | function revertWithCustomError() public pure { 21 | revert CustomError(); 22 | } 23 | 24 | function nestedRevert(Reverter inner, string memory message) public pure { 25 | inner.revertWithMessage(message); 26 | } 27 | 28 | function callThenRevert(Dummy dummy, string memory message) public pure { 29 | dummy.callMe(); 30 | require(false, message); 31 | } 32 | 33 | function revertWithoutReason() public pure { 34 | revert(); 35 | } 36 | } 37 | 38 | contract ConstructorReverter { 39 | constructor(string memory message) { 40 | require(false, message); 41 | } 42 | } 43 | 44 | contract Dummy { 45 | function callMe() public pure returns (string memory) { 46 | return "thanks for calling"; 47 | } 48 | } 49 | 50 | contract ExpectRevertTest is DSTest { 51 | Cheats constant cheats = Cheats(HEVM_ADDRESS); 52 | 53 | function testExpectRevertString() public { 54 | Reverter reverter = new Reverter(); 55 | cheats.expectRevert("revert"); 56 | reverter.revertWithMessage("revert"); 57 | } 58 | 59 | function testExpectRevertConstructor() public { 60 | cheats.expectRevert("constructor revert"); 61 | new ConstructorReverter("constructor revert"); 62 | } 63 | 64 | function testExpectRevertBuiltin() public { 65 | Reverter reverter = new Reverter(); 66 | cheats.expectRevert(abi.encodeWithSignature("Panic(uint256)", 0x11)); 67 | reverter.panic(); 68 | } 69 | 70 | function testExpectRevertCustomError() public { 71 | Reverter reverter = new Reverter(); 72 | cheats.expectRevert(abi.encodePacked(Reverter.CustomError.selector)); 73 | reverter.revertWithCustomError(); 74 | } 75 | 76 | function testExpectRevertNested() public { 77 | Reverter reverter = new Reverter(); 78 | Reverter inner = new Reverter(); 79 | cheats.expectRevert("nested revert"); 80 | reverter.nestedRevert(inner, "nested revert"); 81 | } 82 | 83 | function testExpectRevertCallsThenReverts() public { 84 | Reverter reverter = new Reverter(); 85 | Dummy dummy = new Dummy(); 86 | cheats.expectRevert("called a function and then reverted"); 87 | reverter.callThenRevert(dummy, "called a function and then reverted"); 88 | } 89 | 90 | function testFailExpectRevertErrorDoesNotMatch() public { 91 | Reverter reverter = new Reverter(); 92 | cheats.expectRevert("should revert with this message"); 93 | reverter.revertWithMessage("but reverts with this message"); 94 | } 95 | 96 | function testFailExpectRevertDidNotRevert() public { 97 | Reverter reverter = new Reverter(); 98 | cheats.expectRevert("does not revert, but we think it should"); 99 | reverter.doNotRevert(); 100 | } 101 | 102 | function testExpectRevertNoReason() public { 103 | Reverter reverter = new Reverter(); 104 | cheats.expectRevert(bytes("")); 105 | reverter.revertWithoutReason(); 106 | cheats.expectRevert(); 107 | reverter.revertWithoutReason(); 108 | } 109 | 110 | function testFailExpectRevertDangling() public { 111 | cheats.expectRevert("dangling"); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /testdata/cheats/Fee.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "./Cheats.sol"; 6 | 7 | contract FeeTest is DSTest { 8 | Cheats constant cheats = Cheats(HEVM_ADDRESS); 9 | 10 | function testFee() public { 11 | cheats.fee(10); 12 | assertEq(block.basefee, 10, "fee failed"); 13 | } 14 | 15 | function testFeeFuzzed(uint256 fee) public { 16 | cheats.fee(fee); 17 | assertEq(block.basefee, fee, "fee failed"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /testdata/cheats/Ffi.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "./Cheats.sol"; 6 | 7 | contract FfiTest is DSTest { 8 | Cheats constant cheats = Cheats(HEVM_ADDRESS); 9 | 10 | function testFfi() public { 11 | string[] memory inputs = new string[](3); 12 | inputs[0] = "echo"; 13 | inputs[1] = "-n"; 14 | inputs[2] = "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000966666920776f726b730000000000000000000000000000000000000000000000"; 15 | 16 | bytes memory res = cheats.ffi(inputs); 17 | (string memory output) = abi.decode(res, (string)); 18 | assertEq(output, "ffi works", "ffi failed"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /testdata/cheats/GetCode.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "./Cheats.sol"; 6 | 7 | contract GetCodeTest is DSTest { 8 | Cheats constant cheats = Cheats(HEVM_ADDRESS); 9 | 10 | function testGetCode() public { 11 | bytes memory fullPath = cheats.getCode("../testdata/fixtures/GetCode/WorkingContract.json"); 12 | //bytes memory fileOnly = cheats.getCode("WorkingContract.sol"); 13 | //bytes memory fileAndContractName = cheats.getCode("WorkingContract.sol:WorkingContract"); 14 | 15 | string memory expected = string(bytes(hex"6080604052348015600f57600080fd5b50607c8061001e6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063d1efd30d14602d575b600080fd5b6034602a81565b60405190815260200160405180910390f3fea26469706673582212206740fcc626175d58a151da7fbfca1775ea4d3ababf7f3168347dab89488f6a4264736f6c634300080a0033")); 16 | assertEq( 17 | string(fullPath), 18 | expected, 19 | "code for full path was incorrect" 20 | ); 21 | // TODO: Disabled until we figure out a way to get these variants of the 22 | // cheatcode working during automated tests 23 | //assertEq( 24 | // string(fileOnly), 25 | // expected, 26 | // "code for file name only was incorrect" 27 | //); 28 | //assertEq( 29 | // string(fileAndContractName), 30 | // expected, 31 | // "code for full path was incorrect" 32 | //); 33 | } 34 | 35 | function testFailGetUnlinked() public { 36 | cheats.getCode("UnlinkedContract.sol"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /testdata/cheats/GetNonce.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "./Cheats.sol"; 6 | 7 | contract Foo {} 8 | 9 | contract GetNonceTest is DSTest { 10 | Cheats constant cheats = Cheats(HEVM_ADDRESS); 11 | 12 | function testGetNonce() public { 13 | uint64 nonce1 = cheats.getNonce(address(this)); 14 | new Foo(); 15 | new Foo(); 16 | uint64 nonce2 = cheats.getNonce(address(this)); 17 | assertEq(nonce1 + 2, nonce2); 18 | } 19 | } -------------------------------------------------------------------------------- /testdata/cheats/Label.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "./Cheats.sol"; 6 | 7 | contract LabelTest is DSTest { 8 | Cheats constant cheats = Cheats(HEVM_ADDRESS); 9 | 10 | function testLabel() public { 11 | cheats.label(address(1), "Sir Address the 1st"); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /testdata/cheats/Load.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "./Cheats.sol"; 6 | 7 | contract Storage { 8 | uint256 slot0 = 10; 9 | } 10 | 11 | contract LoadTest is DSTest { 12 | Cheats constant cheats = Cheats(HEVM_ADDRESS); 13 | uint256 slot0 = 20; 14 | Storage store; 15 | 16 | function setUp() public { 17 | store = new Storage(); 18 | } 19 | 20 | function testLoadOwnStorage() public { 21 | uint slot; 22 | assembly { 23 | slot := slot0.slot 24 | } 25 | uint val = uint(cheats.load(address(this), bytes32(slot))); 26 | assertEq(val, 20, "load failed"); 27 | } 28 | 29 | function testLoadOtherStorage() public { 30 | uint val = uint(cheats.load(address(store), bytes32(0))); 31 | assertEq(val, 10, "load failed"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /testdata/cheats/MockCall.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "./Cheats.sol"; 6 | 7 | contract Mock { 8 | function numberA() public pure returns (uint256) { 9 | return 1; 10 | } 11 | 12 | function numberB() public pure returns (uint256) { 13 | return 2; 14 | } 15 | 16 | function add(uint256 a, uint256 b) public pure returns (uint256) { 17 | return a + b; 18 | } 19 | } 20 | 21 | contract NestedMock { 22 | Mock private inner; 23 | 24 | constructor(Mock _inner) { 25 | inner = _inner; 26 | } 27 | 28 | function sum() public view returns (uint256) { 29 | return inner.numberA() + inner.numberB(); 30 | } 31 | } 32 | 33 | contract MockCallTest is DSTest { 34 | Cheats constant cheats = Cheats(HEVM_ADDRESS); 35 | 36 | function testMockGetters() public { 37 | Mock target = new Mock(); 38 | 39 | // pre-mock 40 | assertEq(target.numberA(), 1); 41 | assertEq(target.numberB(), 2); 42 | 43 | cheats.mockCall( 44 | address(target), 45 | abi.encodeWithSelector(target.numberB.selector), 46 | abi.encode(10) 47 | ); 48 | 49 | // post-mock 50 | assertEq(target.numberA(), 1); 51 | assertEq(target.numberB(), 10); 52 | } 53 | 54 | function testMockNested() public { 55 | Mock inner = new Mock(); 56 | NestedMock target = new NestedMock(inner); 57 | 58 | // pre-mock 59 | assertEq(target.sum(), 3); 60 | 61 | cheats.mockCall( 62 | address(inner), 63 | abi.encodeWithSelector(inner.numberB.selector), 64 | abi.encode(9) 65 | ); 66 | 67 | // post-mock 68 | assertEq(target.sum(), 10); 69 | } 70 | 71 | function testMockSelector() public { 72 | Mock target = new Mock(); 73 | assertEq(target.add(5, 5), 10); 74 | 75 | cheats.mockCall( 76 | address(target), 77 | abi.encodeWithSelector(target.add.selector), 78 | abi.encode(11) 79 | ); 80 | 81 | assertEq(target.add(5, 5), 11); 82 | } 83 | 84 | function testMockCalldata() public { 85 | Mock target = new Mock(); 86 | assertEq(target.add(5, 5), 10); 87 | assertEq(target.add(6, 4), 10); 88 | 89 | cheats.mockCall( 90 | address(target), 91 | abi.encodeWithSelector(target.add.selector, 5, 5), 92 | abi.encode(11) 93 | ); 94 | 95 | assertEq(target.add(5, 5), 11); 96 | assertEq(target.add(6, 4), 10); 97 | } 98 | 99 | function testClearMockedCalls() public { 100 | Mock target = new Mock(); 101 | 102 | cheats.mockCall( 103 | address(target), 104 | abi.encodeWithSelector(target.numberB.selector), 105 | abi.encode(10) 106 | ); 107 | 108 | assertEq(target.numberA(), 1); 109 | assertEq(target.numberB(), 10); 110 | 111 | cheats.clearMockedCalls(); 112 | 113 | assertEq(target.numberA(), 1); 114 | assertEq(target.numberB(), 2); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /testdata/cheats/Record.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "./Cheats.sol"; 6 | 7 | contract RecordAccess { 8 | function record() public returns (NestedRecordAccess) { 9 | assembly { 10 | sstore(1, add(sload(1), 1)) 11 | } 12 | 13 | NestedRecordAccess inner = new NestedRecordAccess(); 14 | inner.record(); 15 | 16 | return inner; 17 | } 18 | } 19 | 20 | contract NestedRecordAccess { 21 | function record() public { 22 | assembly { 23 | sstore(2, add(sload(2), 1)) 24 | } 25 | } 26 | } 27 | 28 | contract RecordTest is DSTest { 29 | Cheats constant cheats = Cheats(HEVM_ADDRESS); 30 | 31 | function testRecordAccess() public { 32 | RecordAccess target = new RecordAccess(); 33 | 34 | // Start recording 35 | cheats.record(); 36 | NestedRecordAccess inner = target.record(); 37 | 38 | // Verify Records 39 | (bytes32[] memory reads, bytes32[] memory writes) = cheats.accesses(address(target)); 40 | (bytes32[] memory innerReads, bytes32[] memory innerWrites) = cheats.accesses(address(inner)); 41 | 42 | assertEq(reads.length, 2, "number of reads is incorrect"); 43 | assertEq(reads[0], bytes32(uint256(1)), "key for read 0 is incorrect"); 44 | assertEq(reads[1], bytes32(uint256(1)), "key for read 1 is incorrect"); 45 | 46 | assertEq(writes.length, 1, "number of writes is incorrect"); 47 | assertEq(writes[0], bytes32(uint256(1)), "key for write is incorrect"); 48 | 49 | assertEq(innerReads.length, 2, "number of nested reads is incorrect"); 50 | assertEq(innerReads[0], bytes32(uint256(2)), "key for nested read 0 is incorrect"); 51 | assertEq(innerReads[1], bytes32(uint256(2)), "key for nested read 1 is incorrect"); 52 | 53 | assertEq(innerWrites.length, 1, "number of nested writes is incorrect"); 54 | assertEq(innerWrites[0], bytes32(uint256(2)), "key for nested write is incorrect"); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /testdata/cheats/Roll.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "./Cheats.sol"; 6 | 7 | contract RollTest is DSTest { 8 | Cheats constant cheats = Cheats(HEVM_ADDRESS); 9 | 10 | function testRoll() public { 11 | cheats.roll(10); 12 | assertEq(block.number, 10, "roll failed"); 13 | } 14 | 15 | function testRollFuzzed(uint128 jump) public { 16 | uint pre = block.number; 17 | cheats.roll(block.number + jump); 18 | assertEq(block.number, pre + jump, "roll failed"); 19 | } 20 | 21 | function testRollHash() public { 22 | assertEq(blockhash(block.number), keccak256(abi.encodePacked(block.number)), "initial block hash is incorrect"); 23 | 24 | cheats.roll(5); 25 | bytes32 hash = blockhash(5); 26 | assertTrue(blockhash(5) != 0x0, "new block hash is incorrect"); 27 | 28 | cheats.roll(10); 29 | assertTrue(blockhash(5) != blockhash(10), "block hash collision"); 30 | 31 | cheats.roll(5); 32 | assertEq(blockhash(5), hash, "block 5 changed hash"); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /testdata/cheats/SetNonce.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "./Cheats.sol"; 6 | 7 | contract Foo { 8 | function f() external view returns(uint256) { 9 | return 1; 10 | } 11 | } 12 | 13 | contract SetNonceTest is DSTest { 14 | Cheats constant cheats = Cheats(HEVM_ADDRESS); 15 | Foo public foo; 16 | 17 | function setUp() public { 18 | foo = new Foo(); 19 | } 20 | 21 | function testSetNonce() public { 22 | cheats.setNonce(address(foo), 10); 23 | // makes sure working correctly after mutating nonce. 24 | foo.f(); 25 | assertEq(cheats.getNonce(address(foo)), 10); 26 | foo.f(); 27 | } 28 | 29 | function testFailInvalidNonce() public { 30 | cheats.setNonce(address(foo), 10); 31 | // set lower nonce should fail 32 | cheats.setNonce(address(foo), 5); 33 | } 34 | } -------------------------------------------------------------------------------- /testdata/cheats/Setup.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "./Cheats.sol"; 6 | 7 | contract Victim { 8 | function assertSender(address sender) external { 9 | require(msg.sender == sender, "sender was not pranked"); 10 | } 11 | } 12 | 13 | contract CheatsSetupTest is DSTest { 14 | Cheats constant cheats = Cheats(HEVM_ADDRESS); 15 | Victim victim; 16 | 17 | function setUp() public { 18 | victim = new Victim(); 19 | 20 | cheats.warp(10); 21 | cheats.roll(100); 22 | cheats.fee(1000); 23 | cheats.startPrank(address(1337)); 24 | } 25 | 26 | function testCheatEnvironment() public { 27 | assertEq(block.timestamp, 10, "block timestamp was not persisted from setup"); 28 | assertEq(block.number, 100, "block number was not persisted from setup"); 29 | assertEq(block.basefee, 1000, "basefee was not persisted from setup"); 30 | victim.assertSender(address(1337)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /testdata/cheats/Sign.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "./Cheats.sol"; 6 | 7 | contract SignTest is DSTest { 8 | Cheats constant cheats = Cheats(HEVM_ADDRESS); 9 | 10 | function testSignDigest(uint248 pk, bytes32 digest) public { 11 | cheats.assume(pk != 0); 12 | 13 | (uint8 v, bytes32 r, bytes32 s) = cheats.sign(pk, digest); 14 | address expected = cheats.addr(pk); 15 | address actual = ecrecover(digest, v, r, s); 16 | 17 | assertEq(actual, expected, "digest signer did not match"); 18 | } 19 | 20 | function testSignMessage(uint248 pk, bytes memory message) public { 21 | testSignDigest(pk, keccak256(message)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /testdata/cheats/Store.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "./Cheats.sol"; 6 | 7 | contract Storage { 8 | uint public slot0 = 10; 9 | uint public slot1 = 20; 10 | } 11 | 12 | contract StoreTest is DSTest { 13 | Cheats constant cheats = Cheats(HEVM_ADDRESS); 14 | Storage store; 15 | 16 | function setUp() public { 17 | store = new Storage(); 18 | } 19 | 20 | function testStore() public { 21 | assertEq(store.slot0(), 10, "initial value for slot 0 is incorrect"); 22 | assertEq(store.slot1(), 20, "initial value for slot 1 is incorrect"); 23 | 24 | cheats.store(address(store), bytes32(0), bytes32(uint(1))); 25 | assertEq(store.slot0(), 1, "store failed"); 26 | assertEq(store.slot1(), 20, "store failed"); 27 | } 28 | 29 | function testStoreFuzzed(uint256 slot0, uint256 slot1) public { 30 | assertEq(store.slot0(), 10, "initial value for slot 0 is incorrect"); 31 | assertEq(store.slot1(), 20, "initial value for slot 1 is incorrect"); 32 | 33 | cheats.store(address(store), bytes32(0), bytes32(slot0)); 34 | cheats.store(address(store), bytes32(uint(1)), bytes32(slot1)); 35 | assertEq(store.slot0(), slot0, "store failed"); 36 | assertEq(store.slot1(), slot1, "store failed"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /testdata/cheats/Travel.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "./Cheats.sol"; 6 | 7 | contract ChainIdTest is DSTest { 8 | Cheats constant cheats = Cheats(HEVM_ADDRESS); 9 | 10 | function testChainId() public { 11 | cheats.chainId(10); 12 | assertEq(block.chainid, 10, "chainId switch failed"); 13 | } 14 | 15 | function testChainIdFuzzed(uint128 jump) public { 16 | uint pre = block.chainid; 17 | cheats.chainId(block.chainid + jump); 18 | assertEq(block.chainid, pre + jump, "chainId switch failed"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /testdata/cheats/Warp.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "./Cheats.sol"; 6 | 7 | contract WarpTest is DSTest { 8 | Cheats constant cheats = Cheats(HEVM_ADDRESS); 9 | 10 | function testWarp() public { 11 | cheats.warp(10); 12 | assertEq(block.timestamp, 10, "warp failed"); 13 | } 14 | 15 | function testWarpFuzzed(uint128 jump) public { 16 | uint pre = block.timestamp; 17 | cheats.warp(block.timestamp + jump); 18 | assertEq(block.timestamp, pre + jump, "warp failed"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /testdata/core/Abstract.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | contract TestFixture { 5 | function something() public pure returns (string memory) { 6 | return "something"; 7 | } 8 | } 9 | 10 | abstract contract AbstractTestBase { 11 | TestFixture fixture; 12 | 13 | function testSomething() public { 14 | fixture.something(); 15 | } 16 | } 17 | 18 | contract AbstractTest is AbstractTestBase { 19 | function setUp() public { 20 | fixture = new TestFixture(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /testdata/core/DSStyle.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | 6 | contract DSStyleTest is DSTest { 7 | function testFailingAssertions() public { 8 | emit log_string("assertionOne"); 9 | assertEq(uint(1), uint(2)); 10 | emit log_string("assertionTwo"); 11 | assertEq(uint(3), uint(4)); 12 | emit log_string("done"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /testdata/core/DappToolsParity.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | 6 | contract DappToolsParityTest is DSTest { 7 | function chainId() internal view returns (uint256 id) { 8 | assembly { 9 | id := chainid() 10 | } 11 | } 12 | 13 | function testAddresses() public { 14 | assertEq(msg.sender, 0x00a329c0648769A73afAc7F9381E08FB43dBEA72, "sender account is incorrect"); 15 | assertEq(tx.origin, 0x00a329c0648769A73afAc7F9381E08FB43dBEA72, "origin account is incorrect"); 16 | assertEq(address(this), 0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84, "test contract address is incorrect"); 17 | } 18 | 19 | function testEnvironment() public { 20 | assertEq(chainId(), 99, "chain id is incorrect"); 21 | assertEq(block.number, 0); 22 | assertEq( 23 | blockhash(block.number), 24 | keccak256(abi.encodePacked(block.number)), 25 | "blockhash is incorrect" 26 | ); 27 | assertEq(block.coinbase, 0x0000000000000000000000000000000000000000, "coinbase is incorrect"); 28 | assertEq(block.timestamp, 0, "timestamp is incorrect"); 29 | assertEq(block.difficulty, 0, "difficulty is incorrect"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /testdata/core/FailingSetup.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | 6 | contract FailingSetupTest is DSTest { 7 | event Test(uint256 n); 8 | 9 | function setUp() public { 10 | emit Test(42); 11 | require(false, "setup failed predictably"); 12 | } 13 | 14 | function testFailShouldBeMarkedAsFailedBecauseOfSetup() public { 15 | emit log("setup did not fail"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /testdata/core/LibraryLinking.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | 6 | library Lib { 7 | function plus100(uint256 a) public pure returns (uint256) { 8 | return a + 100; 9 | } 10 | } 11 | 12 | library NestedLib { 13 | function nestedPlus100Plus1(uint256 a) public pure returns (uint256) { 14 | return Lib.plus100(a) + 1; 15 | } 16 | } 17 | 18 | contract LibraryConsumer { 19 | function consume(uint256 a) public pure returns (uint256) { 20 | return Lib.plus100(a); 21 | } 22 | 23 | function consumeNested(uint256 a) public pure returns (uint256) { 24 | return NestedLib.nestedPlus100Plus1(a); 25 | } 26 | } 27 | 28 | contract LibraryLinkingTest is DSTest { 29 | LibraryConsumer consumer; 30 | 31 | function setUp() public { 32 | consumer = new LibraryConsumer(); 33 | } 34 | 35 | function testDirect() public { 36 | assertEq(consumer.consume(1), 101, "library call failed"); 37 | } 38 | 39 | function testNested() public { 40 | assertEq(consumer.consumeNested(1), 102, "nested library call failed"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /testdata/core/MultipleSetup.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | 6 | contract MultipleSetup is DSTest { 7 | function setUp() public { } 8 | 9 | function setup() public { } 10 | 11 | function testFailShouldBeMarkedAsFailedBecauseOfSetup() public { 12 | assert(true); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /testdata/core/PaymentFailure.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "../cheats/Cheats.sol"; 6 | 7 | contract Payable { 8 | function pay() payable public {} 9 | } 10 | 11 | contract PaymentFailureTest is DSTest { 12 | Cheats constant cheats = Cheats(HEVM_ADDRESS); 13 | 14 | function testCantPay() public { 15 | Payable target = new Payable(); 16 | cheats.prank(address(1)); 17 | target.pay{value: 1}(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /testdata/core/Reverting.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | contract RevertingTest { 5 | function testFailRevert() public pure { 6 | require(false, "should revert here"); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /testdata/core/SetupConsistency.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | 6 | contract SetupConsistencyCheck is DSTest { 7 | uint256 two; 8 | uint256 four; 9 | uint256 result; 10 | 11 | function setUp() public { 12 | two = 2; 13 | four = 4; 14 | result = 0; 15 | } 16 | 17 | function testAdd() public { 18 | assertEq(result, 0); 19 | result = two + four; 20 | assertEq(result, 6); 21 | } 22 | 23 | function testMultiply() public { 24 | assertEq(result, 0); 25 | result = two * four; 26 | assertEq(result, 8); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /testdata/fixtures/GetCode/UnlinkedContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | library SmolLibrary { 5 | function add(uint256 a, uint256 b) public pure returns (uint256 c) { 6 | c = a + b; 7 | } 8 | } 9 | 10 | contract UnlinkedContract { 11 | function complicated(uint256 a, uint256 b, uint256 c) public pure returns (uint256 d) { 12 | d = SmolLibrary.add(SmolLibrary.add(a, b), c); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /testdata/fixtures/GetCode/WorkingContract.json: -------------------------------------------------------------------------------- 1 | { 2 | "abi": [ 3 | { 4 | "inputs": [], 5 | "name": "secret", 6 | "outputs": [ 7 | { 8 | "internalType": "uint256", 9 | "name": "", 10 | "type": "uint256" 11 | } 12 | ], 13 | "stateMutability": "view", 14 | "type": "function" 15 | } 16 | ], 17 | "bytecode": { 18 | "object": "0x6080604052348015600f57600080fd5b50607c8061001e6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063d1efd30d14602d575b600080fd5b6034602a81565b60405190815260200160405180910390f3fea26469706673582212206740fcc626175d58a151da7fbfca1775ea4d3ababf7f3168347dab89488f6a4264736f6c634300080a0033", 19 | "sourceMap": "64:69:28:-:0;;;;;;;;;;;;;;;;;;;", 20 | "linkReferences": {} 21 | }, 22 | "deployedBytecode": { 23 | "object": "0x6080604052348015600f57600080fd5b506004361060285760003560e01c8063d1efd30d14602d575b600080fd5b6034602a81565b60405190815260200160405180910390f3fea26469706673582212206740fcc626175d58a151da7fbfca1775ea4d3ababf7f3168347dab89488f6a4264736f6c634300080a0033", 24 | "sourceMap": "64:69:28:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;95:35;;128:2;95:35;;;;;160:25:35;;;148:2;133:18;95:35:28;;;;;;", 25 | "linkReferences": {} 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /testdata/fixtures/GetCode/WorkingContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | contract WorkingContract { 5 | uint256 constant public secret = 42; 6 | } 7 | -------------------------------------------------------------------------------- /testdata/fixtures/SolidityGeneration/GeneratedNamedInterface.sol: -------------------------------------------------------------------------------- 1 | interface test { 2 | event Bar(address indexed x); 3 | event Foo(address x); 4 | 5 | function guess(uint8 n, address x) payable external; 6 | function isComplete() view external returns (bool example, string memory); 7 | } 8 | -------------------------------------------------------------------------------- /testdata/fixtures/SolidityGeneration/GeneratedUnnamedInterface.sol: -------------------------------------------------------------------------------- 1 | interface Interface { 2 | event Bar(address indexed x); 3 | event Foo(address x); 4 | 5 | function guess(uint8 n, address x) payable external; 6 | function isComplete() view external returns (bool example, string memory); 7 | } 8 | -------------------------------------------------------------------------------- /testdata/fixtures/SolidityGeneration/InterfaceABI.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": false, 7 | "internalType": "address", 8 | "name": "x", 9 | "type": "address" 10 | } 11 | ], 12 | "name": "Foo", 13 | "type": "event" 14 | }, 15 | { 16 | "anonymous": false, 17 | "inputs": [ 18 | { 19 | "indexed": true, 20 | "internalType": "address", 21 | "name": "x", 22 | "type": "address" 23 | } 24 | ], 25 | "name": "Bar", 26 | "type": "event" 27 | }, 28 | { 29 | "inputs": [ 30 | { 31 | "internalType": "uint8", 32 | "name": "n", 33 | "type": "uint8" 34 | }, 35 | { 36 | "internalType": "address", 37 | "name": "x", 38 | "type": "address" 39 | } 40 | ], 41 | "name": "guess", 42 | "outputs": [], 43 | "stateMutability": "payable", 44 | "type": "function" 45 | }, 46 | { 47 | "inputs": [], 48 | "name": "isComplete", 49 | "outputs": [ 50 | { 51 | "internalType": "bool", 52 | "name": "example", 53 | "type": "bool" 54 | }, 55 | { 56 | "internalType": "string", 57 | "name": "", 58 | "type": "string" 59 | } 60 | ], 61 | "stateMutability": "view", 62 | "type": "function" 63 | } 64 | ] 65 | -------------------------------------------------------------------------------- /testdata/fork/Fork.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.6.12; 3 | 4 | import "ds-test/test.sol"; 5 | import "./DssExecLib.sol"; 6 | 7 | interface Cheats { 8 | function store(address account, bytes32 slot, bytes32 value) external; 9 | } 10 | 11 | 12 | interface IWETH { 13 | function deposit() external payable; 14 | function balanceOf(address) external view returns (uint); 15 | } 16 | 17 | // A minimal contract. We test if it is deployed correctly 18 | contract DummyContract { 19 | address public deployer; 20 | constructor() public { 21 | deployer = msg.sender; 22 | } 23 | } 24 | 25 | 26 | contract ForkTest is DSTest { 27 | address constant DAI_TOKEN_ADDR = 0x6B175474E89094C44Da98b954EedeAC495271d0F; 28 | address constant WETH_TOKEN_ADDR = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; 29 | 30 | function testReadState() public { 31 | ERC20 DAI = ERC20(DAI_TOKEN_ADDR); 32 | assertEq(uint(DAI.decimals()), uint(18), "Failed to read DAI token decimals."); 33 | } 34 | 35 | function testDeployContract() public { 36 | DummyContract dummy = new DummyContract(); 37 | uint size; 38 | address DummyAddress = address(dummy); 39 | assembly { 40 | size := extcodesize(DummyAddress) 41 | } 42 | assertGt(size, 0, "Deploying dummy contract failed. Deployed size of zero"); 43 | assertEq(dummy.deployer(), address(this), "Calling the Dummy contract failed to return expected value"); 44 | } 45 | 46 | function testCheatcode() public { 47 | Cheats cheatvm = Cheats(HEVM_ADDRESS); 48 | IWETH WETH = IWETH(WETH_TOKEN_ADDR); 49 | bytes32 value = bytes32(uint(1)); 50 | // "0x3617319a054d772f909f7c479a2cebe5066e836a939412e32403c99029b92eff" is the slot storing the balance of zero address for the weth contract 51 | // `cast index address uint 0x0000000000000000000000000000000000000000 3` 52 | bytes32 zero_address_balance_slot = 0x3617319a054d772f909f7c479a2cebe5066e836a939412e32403c99029b92eff; 53 | cheatvm.store(WETH_TOKEN_ADDR, zero_address_balance_slot, value); 54 | assertEq(WETH.balanceOf(0x0000000000000000000000000000000000000000), 1, "Cheatcode did not change value at the storage slot."); 55 | } 56 | 57 | function testPredeployedLibrary() public { 58 | assertEq(DssExecLib.dai(), DAI_TOKEN_ADDR, "Failed to read state from predeployed library"); 59 | } 60 | 61 | function testDepositWeth() public { 62 | IWETH WETH = IWETH(WETH_TOKEN_ADDR); 63 | WETH.deposit{value: 1000}(); 64 | assertEq(WETH.balanceOf(address(this)), 1000, "WETH balance is not equal to deposited amount."); 65 | } 66 | } -------------------------------------------------------------------------------- /testdata/fuzz/Fuzz.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | 6 | contract FuzzTest is DSTest { 7 | constructor() { 8 | emit log("constructor"); 9 | } 10 | 11 | function setUp() public { 12 | emit log("setUp"); 13 | } 14 | 15 | function testFailFuzz(uint8 x) public { 16 | emit log("testFailFuzz"); 17 | require(x == 5, "should revert"); 18 | } 19 | 20 | function testSuccessfulFuzz(uint128 a, uint128 b) public { 21 | emit log("testSuccessfulFuzz"); 22 | assertEq(uint256(a) + uint256(b), uint256(a) + uint256(b)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testdata/fuzz/FuzzNumbers.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity >=0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | 6 | // See https://github.com/gakonst/foundry/pull/735 for context 7 | contract FuzzNumbersTest is DSTest { 8 | function testPositive(uint256) public { 9 | assertTrue(true); 10 | } 11 | 12 | function testNegativeHalf(uint256 val) public { 13 | assertTrue(val < 2 ** 128 - 1); 14 | } 15 | 16 | function testNegative0(uint256 val) public { 17 | assertTrue(val != 0); 18 | } 19 | 20 | function testNegative2(uint256 val) public { 21 | assertTrue(val != 2); 22 | } 23 | 24 | function testNegative2Max(uint256 val) public { 25 | assertTrue(val != type(uint256).max - 2); 26 | } 27 | 28 | function testNegativeMax(uint256 val) public { 29 | assertTrue(val != type(uint256).max); 30 | } 31 | 32 | function testEquality(uint256 x, uint256 y) public { 33 | uint256 xy; 34 | 35 | unchecked { 36 | xy = x * y; 37 | } 38 | 39 | if ((x != 0 && xy / x != y)) return; 40 | 41 | assertEq(((xy - 1) / 1e18) + 1, (xy - 1) / (1e18 + 1)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /testdata/logs/DebugLogs.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.8.0; 2 | 3 | import "ds-test/test.sol"; 4 | 5 | contract DebugLogsTest is DSTest { 6 | constructor() { 7 | emit log_uint(0); 8 | } 9 | 10 | function setUp() public { 11 | emit log_uint(1); 12 | } 13 | 14 | function test1() public { 15 | emit log_uint(2); 16 | } 17 | 18 | function test2() public { 19 | emit log_uint(3); 20 | } 21 | 22 | function testFailWithRevert() public { 23 | Fails fails = new Fails(); 24 | emit log_uint(4); 25 | fails.failure(); 26 | } 27 | 28 | function testFailWithRequire() public { 29 | emit log_uint(5); 30 | require(false); 31 | } 32 | } 33 | 34 | contract Fails is DSTest { 35 | function failure() public { 36 | emit log_uint(100); 37 | revert(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /testdata/trace/ConflictingSignatures.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.8.0; 2 | 3 | import "ds-test/test.sol"; 4 | import "../cheats/Cheats.sol"; 5 | 6 | contract ReturnsNothing { 7 | function func() public pure {} 8 | } 9 | 10 | contract ReturnsString { 11 | function func() public pure returns (string memory) { 12 | return "string"; 13 | } 14 | } 15 | 16 | contract ReturnsUint { 17 | function func() public pure returns (uint256) { 18 | return 1; 19 | } 20 | } 21 | 22 | contract ConflictingSignaturesTest is DSTest { 23 | ReturnsNothing retsNothing; 24 | ReturnsString retsString; 25 | ReturnsUint retsUint; 26 | 27 | function setUp() public { 28 | retsNothing = new ReturnsNothing(); 29 | retsString = new ReturnsString(); 30 | retsUint = new ReturnsUint(); 31 | } 32 | 33 | /// Tests that traces are decoded properly when multiple 34 | /// functions have the same 4byte signature, but different 35 | /// return values. 36 | function testTraceWithConflictingSignatures() public { 37 | retsNothing.func(); 38 | retsString.func(); 39 | retsUint.func(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /testdata/trace/Trace.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.8.0; 2 | 3 | import "ds-test/test.sol"; 4 | import "../cheats/Cheats.sol"; 5 | 6 | contract RecursiveCall { 7 | TraceTest factory; 8 | 9 | event Depth(uint256 depth); 10 | event ChildDepth(uint256 childDepth); 11 | event CreatedChild(uint256 childDepth); 12 | 13 | constructor(address _factory) { 14 | factory = TraceTest(_factory); 15 | } 16 | 17 | function recurseCall(uint256 neededDepth, uint256 depth) public returns (uint256) { 18 | if (depth == neededDepth) { 19 | this.negativeNum(); 20 | return neededDepth; 21 | } 22 | 23 | uint256 childDepth = this.recurseCall(neededDepth, depth + 1); 24 | emit ChildDepth(childDepth); 25 | 26 | this.someCall(); 27 | emit Depth(depth); 28 | 29 | return depth; 30 | } 31 | 32 | function recurseCreate(uint256 neededDepth, uint256 depth) public returns (uint256) { 33 | if (depth == neededDepth) { 34 | return neededDepth; 35 | } 36 | 37 | RecursiveCall child = factory.create(); 38 | emit CreatedChild(depth + 1); 39 | 40 | uint256 childDepth = child.recurseCreate(neededDepth, depth + 1); 41 | emit ChildDepth(childDepth); 42 | emit Depth(depth); 43 | 44 | return depth; 45 | } 46 | 47 | function someCall() public pure {} 48 | 49 | function negativeNum() public pure returns (int256) { 50 | return -1000000000; 51 | } 52 | } 53 | 54 | contract TraceTest is DSTest { 55 | Cheats constant cheats = Cheats(HEVM_ADDRESS); 56 | 57 | uint256 nodeId = 0; 58 | RecursiveCall first; 59 | 60 | function setUp() public { 61 | first = this.create(); 62 | } 63 | 64 | function create() public returns (RecursiveCall) { 65 | RecursiveCall node = new RecursiveCall(address(this)); 66 | cheats.label( 67 | address(node), 68 | string(abi.encodePacked("Node ", uintToString(nodeId++))) 69 | ); 70 | 71 | return node; 72 | } 73 | 74 | function testRecurseCall() public { 75 | first.recurseCall(8, 0); 76 | } 77 | 78 | function testRecurseCreate() public { 79 | first.recurseCreate(8, 0); 80 | } 81 | } 82 | 83 | function uintToString(uint256 value) pure returns (string memory) { 84 | // Taken from OpenZeppelin 85 | if (value == 0) { 86 | return "0"; 87 | } 88 | uint256 temp = value; 89 | uint256 digits; 90 | while (temp != 0) { 91 | digits++; 92 | temp /= 10; 93 | } 94 | bytes memory buffer = new bytes(digits); 95 | while (value != 0) { 96 | digits -= 1; 97 | buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); 98 | value /= 10; 99 | } 100 | return string(buffer); 101 | } 102 | -------------------------------------------------------------------------------- /ui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ui" 3 | version = "0.2.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | readme = "../README.md" 7 | repository = "https://github.com/gakonst/foundry" 8 | keywords = ["ethereum", "web3", "evm", "debugger"] 9 | 10 | [dependencies] 11 | crossterm = "0.22.1" 12 | tui = { version = "0.16.0", default-features = false, features = ["crossterm"] } 13 | eyre = "0.6.5" 14 | hex = "0.4.3" 15 | ethers = { git = "https://github.com/gakonst/ethers-rs" } 16 | forge = { path = "../forge" } 17 | revm = { package = "revm", git = "https://github.com/bluealloy/revm", default-features = false, features = ["std", "k256", "with-serde"] } 18 | -------------------------------------------------------------------------------- /ui/src/op_effects.rs: -------------------------------------------------------------------------------- 1 | /// Maps an opcode and returns a vector of named affected indices 2 | pub fn stack_indices_affected(op: u8) -> Vec<(usize, &'static str)> { 3 | match op { 4 | 0x01 => vec![(0, "a"), (1, "b")], 5 | 0x02 => vec![(0, "a"), (1, "b")], 6 | 0x03 => vec![(0, "a"), (1, "b")], 7 | 0x04 => vec![(0, "a"), (1, "b")], 8 | 0x05 => vec![(0, "a"), (1, "b")], 9 | 0x06 => vec![(0, "a"), (1, "b")], 10 | 0x07 => vec![(0, "a"), (1, "b")], 11 | 0x08 => vec![(0, "a"), (1, "b"), (2, "N")], 12 | 0x09 => vec![(0, "a"), (1, "b"), (2, "N")], 13 | 0x0a => vec![(0, "a"), (1, "exponent")], 14 | 0x0b => vec![(0, "b"), (1, "x")], 15 | 0x10 => vec![(0, "a"), (1, "b")], 16 | 0x11 => vec![(0, "a"), (1, "b")], 17 | 0x12 => vec![(0, "a"), (1, "b")], 18 | 0x13 => vec![(0, "a"), (1, "b")], 19 | 0x14 => vec![(0, "a"), (1, "b")], 20 | 0x15 => vec![(0, "a")], 21 | 0x16 => vec![(0, "a"), (1, "b")], 22 | 0x17 => vec![(0, "a"), (1, "b")], 23 | 0x18 => vec![(0, "a"), (1, "b")], 24 | 0x19 => vec![(0, "a")], 25 | 0x1a => vec![(0, "i"), (1, "x")], 26 | 0x1b => vec![(0, "shift"), (1, "value")], 27 | 0x1c => vec![(0, "shift"), (1, "value")], 28 | 0x1d => vec![(0, "shift"), (1, "value")], 29 | 0x20 => vec![(0, "offset"), (1, "size")], 30 | 0x31 => vec![(0, "address")], 31 | 0x35 => vec![(0, "offset")], 32 | 0x37 => vec![(0, "destOffset"), (1, "offset"), (2, "size")], 33 | 0x39 => vec![(0, "destOffset"), (1, "offset"), (2, "size")], 34 | 0x3b => vec![(0, "address")], 35 | 0x3c => vec![(0, "address"), (1, "destOffset"), (2, "offset"), (3, "size")], 36 | 0x3e => vec![(0, "destOffset"), (1, "offset"), (2, "size")], 37 | 0x3f => vec![(0, "address")], 38 | 0x40 => vec![(0, "blockNumber")], 39 | 0x50 => vec![(0, "y")], 40 | 0x51 => vec![(0, "offset")], 41 | 0x52 => vec![(0, "offset"), (1, "value")], 42 | 0x53 => vec![(0, "offset"), (1, "value")], 43 | 0x54 => vec![(0, "key")], 44 | 0x55 => vec![(0, "key"), (1, "value")], 45 | 0x56 => vec![(0, "jump_to")], 46 | 0x57 => vec![(0, "jump_to"), (1, "if")], 47 | 0x80 => vec![(0, "dup_value")], 48 | 0x81 => vec![(1, "dup_value")], 49 | 0x82 => vec![(2, "dup_value")], 50 | 0x83 => vec![(3, "dup_value")], 51 | 0x84 => vec![(4, "dup_value")], 52 | 0x85 => vec![(5, "dup_value")], 53 | 0x86 => vec![(6, "dup_value")], 54 | 0x87 => vec![(7, "dup_value")], 55 | 0x88 => vec![(8, "dup_value")], 56 | 0x89 => vec![(9, "dup_value")], 57 | 0x8a => vec![(10, "dup_value")], 58 | 0x8b => vec![(11, "dup_value")], 59 | 0x8c => vec![(12, "dup_value")], 60 | 0x8d => vec![(13, "dup_value")], 61 | 0x8e => vec![(14, "dup_value")], 62 | 0x8f => vec![(15, "dup_value")], 63 | 0x90 => vec![(0, "a"), (1, "swap_value")], 64 | 0x91 => vec![(0, "a"), (2, "swap_value")], 65 | 0x92 => vec![(0, "a"), (3, "swap_value")], 66 | 0x93 => vec![(0, "a"), (4, "swap_value")], 67 | 0x94 => vec![(0, "a"), (5, "swap_value")], 68 | 0x95 => vec![(0, "a"), (6, "swap_value")], 69 | 0x96 => vec![(0, "a"), (7, "swap_value")], 70 | 0x97 => vec![(0, "a"), (8, "swap_value")], 71 | 0x98 => vec![(0, "a"), (9, "swap_value")], 72 | 0x99 => vec![(0, "a"), (10, "swap_value")], 73 | 0x9a => vec![(0, "a"), (11, "swap_value")], 74 | 0x9b => vec![(0, "a"), (12, "swap_value")], 75 | 0x9c => vec![(0, "a"), (13, "swap_value")], 76 | 0x9d => vec![(0, "a"), (14, "swap_value")], 77 | 0x9e => vec![(0, "a"), (15, "swap_value")], 78 | 0x9f => vec![(0, "a"), (16, "swap_value")], 79 | 0xa0 => vec![(0, "offset"), (1, "size")], 80 | 0xa1 => vec![(0, "offset"), (1, "size"), (2, "topic")], 81 | 0xa2 => vec![(0, "offset"), (1, "size"), (2, "topic1"), (3, "topic2")], 82 | 0xa3 => vec![(0, "offset"), (1, "size"), (2, "topic1"), (3, "topic2"), (4, "topic3")], 83 | 0xa4 => vec![ 84 | (0, "offset"), 85 | (1, "size"), 86 | (2, "topic1"), 87 | (3, "topic2"), 88 | (4, "topic3"), 89 | (5, "topic4"), 90 | ], 91 | 0xf0 => vec![(0, "value"), (1, "offset"), (2, "size")], 92 | 0xf1 => vec![ 93 | (0, "gas"), 94 | (1, "address"), 95 | (2, "value"), 96 | (3, "argsOffset"), 97 | (4, "argsSize"), 98 | (5, "retOffset"), 99 | (6, "retSize"), 100 | ], 101 | 0xf2 => vec![ 102 | (0, "gas"), 103 | (1, "address"), 104 | (2, "value"), 105 | (3, "argsOffset"), 106 | (4, "argsSize"), 107 | (5, "retOffset"), 108 | (6, "retSize"), 109 | ], 110 | 0xf3 => vec![(0, "offset"), (1, "size")], 111 | 0xf4 => vec![ 112 | (0, "gas"), 113 | (1, "address"), 114 | (2, "argsOffset"), 115 | (3, "argsSize"), 116 | (4, "retOffset"), 117 | (5, "retSize"), 118 | ], 119 | 0xf5 => vec![(0, "value"), (1, "offset"), (2, "size"), (3, "salt")], 120 | 0xfa => vec![ 121 | (0, "gas"), 122 | (1, "address"), 123 | (2, "argsOffset"), 124 | (3, "argsSize"), 125 | (4, "retOffset"), 126 | (5, "retSize"), 127 | ], 128 | 0xfd => vec![(0, "offset"), (1, "size")], 129 | 0xff => vec![(0, "address")], 130 | _ => vec![], 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foundry-utils" 3 | version = "0.2.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | readme = "README.md" 7 | repository = "https://github.com/gakonst/foundry" 8 | 9 | [dependencies] 10 | ethers-core = { git = "https://github.com/gakonst/ethers-rs", default-features = false } 11 | ethers-etherscan = { git = "https://github.com/gakonst/ethers-rs", default-features = false } 12 | ethers-addressbook = { git = "https://github.com/gakonst/ethers-rs", default-features = false } 13 | ethers-providers = { git = "https://github.com/gakonst/ethers-rs", default-features = false } 14 | ethers-solc = { git = "https://github.com/gakonst/ethers-rs", default-features = false } 15 | tracing-subscriber = { version = "0.3", default-features = false, features = ["env-filter", "fmt"], optional = true } 16 | 17 | 18 | eyre = { version = "0.6.5", default-features = false } 19 | hex = "0.4.3" 20 | reqwest = { version = "0.11.8", default-features = false, features = ["json", "rustls"] } 21 | rustc-hex = { version = "2.1.0", default-features = false } 22 | serde = "1.0.132" 23 | serde_json = { version = "1.0.67", default-features = false } 24 | tokio = { version = "1.12.0", features = ["rt-multi-thread", "macros"] } 25 | rlp = "0.5.1" 26 | 27 | 28 | [dev-dependencies] 29 | ethers = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["solc-full"] } 30 | 31 | 32 | [features] 33 | test = ["tracing-subscriber"] 34 | -------------------------------------------------------------------------------- /utils/README.md: -------------------------------------------------------------------------------- 1 | # foundry-utils 2 | 3 | Helper methods for interacting with EVM ABIs 4 | --------------------------------------------------------------------------------